MCP -The Golden Key for AI Automation
MCP looks like the Golden Key to unlock the full potential of LLMs. From Google to Microsoft (MCP on Windows 11) and everywhere in between
Model Context Protocol — MCP is attracting plenty of hype and seems over-specified. It already has some false starts, like SSE (which is getting deprecated in favour of HTTP Streaming due to Cloud hosting cost).
However, it fills a void for LLM integration with API calls and is popular enough to become the one standard to rule them all. It is simply a way of publishing your APIs, like via Swagger, so that LLMs can understand it and can generate invocations of the API with arguments; almost like REST; only this is via JSON-RPC and is transport independent, unlike REST or Protobuf/gRPC, which are based on HTTP. (However, all remote MCP servers are HTTP-based, while local ones communicate via STDIO -standard input/output named pipes).
Demystifying MCP with an example of a simple Calculator MCP Server Large Language Models (LLMs) are very powerful — but they’re not designed to be calculators. They often struggle with precise arithmetic or logic-heavy operations. So what if, instead of forcing them to compute, we gave them the right tool whenever they encountered a calculation task?
This idea isn’t new. It’s been implemented in many ways already. From LLM’s gerenating code to do the calculations and then extracting that code and running it in a python sandbox and giving the result; or LLM generating the URL or JSON for calling some other service via REST or GRPC and then extracting the URL or JSON and invoking the service and getting the result; or maybe some other esoteric way. With MCP, this tool call is standardised. It is based on JSON-RPC. It has two flavours; local tools can be called via STDIO piping. Remote tools via HTTP.
A simplified way of thinking of MCP via HTTP is that it is almost like REST. But the biggest difference is that REST is stateless, whereas MCP is statefull. And that impacts a lot of design decisions.
MCP is a simple concept that even non-technical people can grasp. I have hosted a simple MCP Server in Python freely via HuggingFace Spaces using Docker. The MCP spec is complex, and you need a framework like FastMCP to build out the Server for Python.
from fastmcp import FastMCP
mcp = FastMCP()
# Assume that this is the tool you want to expose
# Give all the types and description
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
if __name__ == "__main__":
# take host and port from command line
import argparse
parser = argparse.ArgumentParser(description="Run FastMCP server")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host address (default: 0.0.0.0)")
parser.add_argument("--port", type=int, default=7860, help="Port number (default: 7860)")
args = parser.parse_args()
mcp.run(
transport="streamable-http", # https://github.com/modelcontextprotocol/python-sdk/?tab=readme-ov-file#streamable-http-transport
host=args.host,
port=args.port,
path="/mcp",
log_level="debug",
)
Follow the links to understand. It exposes the add method I have an MCP Client hosted via Colab, which you should also be able to run and test.
It is easier to understand the flow from the Client side. First, we connect to the MCP Server and ask it to list the tools.
async with Client("https://alexcpn-mcpserver-demo.hf.space/mcp/") as client:
await client.ping()
# List available tools
tools = await client.list_tools()
print("Available tools:", tools)
tool_result = await client.call_tool("add", {"a": "1", "b": "2"})
print("Tool result:", tool_result)
The output is as follows.
Available tools: [Tool(name='add', description='Add two numbers', inputSchema={'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}, 'required': ['a', 'b'], 'type': 'object'}, annotations=None)]
Without a framework, this is how we advertise the tool details on the server, and you may have noticed that this is what the Client gets. Frameworks like FastMCP infer this from the type information.
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="add",
description=(
"add two numbers and return the result. "
),
inputSchema={
"type": "object",
"required": ["a", "b"],
"properties": {
"a": {
"type": "integer",
"description": "The first number to add",
},
"b": {
"type": "integer",
"description": "The second number to add",
...
Next, we give this output ‘tool_result’ to LLM and ask it to frame a proper JSON output for the tool call with the relevant arguments.
a = 123124522
b= 865734234
question = f"Using the tools available {tool_result} frame the JSON RPC call to the tool add with a={a} and b={b}, do not add anything else to the output" + \
"here is the JSON RPC call format {{\"method\": \"<method name>\", \"params\": {{\"<param 1 name>\": {<param 1 value>}, \"<param 2 name>\": {<param 2 value>} etc }}}}"
# Use a simple model like gpt-3.5-turbo
completion = openai_client.chat.completions.create(
model="gpt-3.5-turbo", messages=[
{"role": "user", "content":question }
]
)
# Print the response
print("LLM response:", tool_call)
print(tool_call["method"], tool_call["params"])
We got the output in JSON as shown below.
LLM response: {'method': 'add', 'params': {'a': 123124522, 'b': 865734234}} add {'a': 123124522, 'b': 865734234}
We send this JSON to the LLM Server via the MCP Client tool call API.
tool_result = await client.call_tool(tool_call["method"], tool_call["params"]) print("Tool result:", tool_result)
The result from the MCP server is shown below.
Tool result: [TextContent(type='text', text='988858756', annotations=None)]
Simple right — LLM calling a tool — rather LLM getting information about a tool and generating the call signature in JSON format, and our program using the MCP client to actually call the MCP server. Authorisation in MCP
MCP uses the OAuth2 flow for authorisation. This means that you don’t have to share your username and/or password with any MCP Client. MCP Client redirects you to your resource server, say a Photo store or Gmail or any other service, and you log in to the server with your credentials.
1. MCP Client --> say Gmail MCP Server (establish unsecure connection) 2. Request login() , MCP server sends a link to click with above sessionid 3. User clicks the link (or browser opens with the link) to Gmail Server 4. User enters Gmail Credentials and is shown that they are logged in 5. Internally Gmail servers calls the Gmail MCP Server with the session id (redirect url)
and tells the MCP Server that the Session id is authenicated
6. Now MCP Client --> MCP Server session is authenticated 7. Other calls that need authenication do not need any token or secret key
MCP Client can request users emails etc
Once authenticated, the MCP Server gets a callback from the resource server, say Gmail, via the redirect URL, and it marks internally that the SSE or HTTP Streaming session is authenticated. The rest of the calls can then be processed as with authentication.
We will see this flow using the recently launched MCP Server of the discount stock brokerage Zerodha Kite. Zerodha is one of India’s largest discount brokerages, similar to the Robinhood trading platform in the US. They recently introduced the Kite MCP server, exposing your portfolio and other actions for integration with AI assistants.
With this integration, you can query your holdings, research about your stocks and do other Agentic workflows with thick clients like Claude-Desktop.
While their documentation showcases tools like Claude-Desktop integration with the MCP Server, it often leaves developers wondering: How do I authenticate my own custom MCP Client code without passing login credentials or my secret tokens??Especially if I am developing a thick like Claude Desktop or some Agentic AI based on an authenticated MCP Server like Kite.
This section aims to simplify the flow of authenticating your Python MCP client with an authentication-enabled MCP server. We use Zerodha Kite’s MCP server as an example, as nothing needs more security than financial services.
The Challenge: A New Authentication Paradigm
Traditional Zerodha Kite Connect API authentication typically involves obtaining your own API Key and Secret, then performing an OAuth 2.0 flow (often with PKCE for public clients) to get an access_token directly. However, the MCP server introduces an abstraction layer.
However, with the MCP Authorisation flow, which is detailed here, the OAuth2 protocol is followed.
I am not an expert in this, but it is kind of simple to understand this way — instead of sharing your login credentials (user name and password) of say Kite with a third party (in this case — my python MCP client for getting your Kite holdings), the third party first creates an non authenticated session with Kite MCP Server and then redirects you to your favourite trusted web browser to authenticate with Kite. Once you authenticate with Kite externally, the Kite MCP Server marks the session that your third party opened earlier as authenticated. You can now go back to the session and do all authenticated tasks, like get your holdings.
The token is active for the time the session is active, and it is not stored or used in cookies, etc. More details below
Deconstructing the MCP Authentication Flow
The core of the MCP authentication lies in a sophisticated OAuth flow that leverages Zerodha’s backend services as an intermediary. Here’s a step-by-step breakdown of how your Python fastmcp client authenticates:
- Establishing the SSE Session: Your Python fastmcp client initiates a Server-Sent Events (SSE) connection to the MCP Server (e.g., https://mcp.kite.trade/sse). At this point, the MCP Server establishes a unique, persistent session with your client and creates an internal session_id to track this specific connection.
Your FastMCP Client --> SSE Session --> MCP Server (https://mcp.kite.trade/sse)
async def main():
from fastmcp import Client
from fastmcp.client.transports import SSETransport
transport = SSETransport(
url="https://mcp.kite.trade/sse",
headers={}
)
async with Client(transport) as client:
print("Connected to fastmcp client.")
# 3. Call the 'login' tool
2. Initiating the Login Command:
Once the SSE session is open, your fastmcp client calls the login() tool. This isn't a direct authentication call but a request to the MCP Server to initiate the external user authentication process.
- Internally, the MCP Server (Kite MCP Server) generates a new, temporary token session_id specifically for this OAuth login attempt and maps it to your active SSE session internally.
- It then returns Login URL to your fastmcp client. This URL is crucial and contains your api_key=kitemcp and the generated session_id within its redirect_params.
async with Client(transport) as client:
print("Connected to fastmcp client.")
# 3. Call the 'login' tool
print("Calling fastmcp 'login' tool...")
login_result = await client.call_tool("login", {})
print("Login result from fastmcp:", login_result)
# 4. Extract and display the URL to the user
login_url = None
if isinstance(login_result, list) and login_result:
for item in login_result:
if hasattr(item, 'type') and item.type == 'text' and 'URL:' in item.text:
url_start_index = item.text.find('URL: ') + len('URL: ')
login_url = item.text[url_start_index:].strip()
break
Output below
Example Login URL: https://kite.zerodha.com/connect/login?api_key=kitemcp&v=3&redirect_params=session_id%3D44ce2d96-710b-4442-bf57-cd0d5d0e4050
3. External Browser Authentication: Our Python script then opens this Login URL in the user's default web browser (e.g., Chrome or Firefox). You can also copy-paste this and do it yourself. The user proceeds to authenticate directly with Zerodha Kite, providing their credentials and completing any 2FA steps.
if login_url:
print("\n=======================================================")
print(" Please open this URL in your browser to login to Kite:")
print(f" {login_url}")
print("=======================================================\n")
# Open the URL automatically for convenience
webbrowser.open(login_url)
4. The MCP Server’s Orchestration (The “Hidden” Handshake): Upon successful authentication, Zerodha Kite’s main OAuth Authorisation Server redirects the user’s browser back to a pre-configured redirect_uri. Critically, this redirect_uri points back to an endpoint within Zerodha's MCP Server infrastructure, not to your local machine. You don’t have to spin up an HTTP Server and handle the callback.
- This redirect includes an authorisation code (request_token) and the session_id that the MCP Server is originally embedded in the Login URL.
- Internal Mapping: Once the Kite login authentication is done, it calls the Kite MCP Server (redirect URI) with the session_id . The Kite MCP Server uses the session-id to look up which of its active SSE sessions (your fastmcp client's connection) initiated this login and marks the session as authenticated
5. Implicit Session Update: The MCP Server does not directly send this access_token back to your fastmcp client. Instead, it implicitly updates the authentication state of your SSE session, signalling that your client is now authenticated.
6. Proceeding with Authenticated Calls: After the user confirms they’ve logged in (e.g., by pressing Enter in your terminal), the fastmcp client can proceed to call other API tools like getHoldings.
- When you call client.call_tool("get_holdings", {}), the fastmcp client sends this command over the already authenticated SSE session to the MCP Server. The mcp.kite.trade backend, which now securely holds the access_token For that specific session, use it to make the actual get_holdings API call to Zerodha's core Kite API on your behalf.
- The results are then streamed back to your fastmcp client via the SSE connection.
webbrowser.open(login_url)
# 5. Wait for user confirmation (crucial step for CLI)
input("Press Enter after you have successfully logged in to Kite in your browser...")
print("User confirmed login. Attempting to proceed with fastmcp calls.")
print("Session details after login:")
print(client.session)
# Give a small buffer time for fastmcp to potentially synchronize or detect the login.
await asyncio.sleep(2)
# 6. Call subsequent tools (assuming fastmcp's session is now updated)
print("Calling fastmcp 'get_holdings' tool...")
try:
holdings = await client.call_tool("get_holdings", {})
print("Your holdings:", holdings)
The Key Takeaway: No API Secret,Token Secret in Your Client A fundamental aspect of this flow, and a significant security benefit, is that your Python client never needs to know or store the API_Secret associated with the kitemcp API key. The kitemcp API_Key is public, and its corresponding API_Secret is securely managed by Zerodha's MCP service. This makes your fastmcp client a true "public client" in the OAuth ecosystem, simplifying deployment and enhancing security.
Practical Implementation (Code Snippet) https://github.com/alexcpn/kite-mcp-client
Conclusion
MCP seems to become the one standard to rule them all. It seems to be the key to unlock further automation with LLMs.
Read the full article here: https://pub.towardsai.net/mcp-the-golden-key-for-ai-automation-1f5d41cf5241