Possible to launch a server and client in separate processes?
When looking at the examples, it seems like you always need to launch the server and client in the same script, because they share the read/write variables.
Is there a way to launch these two pieces in their own scripts? If so, is it documented? This feels like an extremely common use case that might be missing.
Trying to write an MCP server integration for tools in llama-index and realized I can't figure it out.
Thanks for any help!
I am not sure I fully understand. MCP is a client / server architecture. Clients and Servers operate independently and speak over a transport to each other.
Implementing a client
If you want to implement a client, you likely want to connect to servers either via STDIO or HTTP+SSE. For STDIO you spawn a server executable via stdio_client(parameters). This can be done at any point. Commonly users would define a set of servers and you spawn for each one a client + session. For HTTP+SSE you can start a client + session separately and just listen on a port for a connection.
Since you mentioned 'in the same script'. If you want to have a stdio client, you need to spawn a server. However, this is not the same script. STDIO Servers are just programs that listen for JSON-RPC messages on STDIN and write JSON-RPC messages to stdout.
It might help to take https://modelcontextprotocol.io/llms-full.txt, put it into Claude and ask the model for more help to understand the concept.
Implementing a server
If you are only interested in implementing a server, you never need to start a client. I would recommend a framework like https://github.com/jlowin/fastmcp for ease of use.
I hope this helps. Let me know if you have more questions.,
@dsp-ant yea I guess I'm just confused. The example in the readme is something like:
server_params = StdioServerParameters(
command="python",
args=["example_server.py"],
env=None,
)
# launches the server
async with stdio_client(server_params) as (read, write):
# launches the client
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()
...
But in this example, it seems like the client depends on variables that you can only get from launching the server? How do you launch them separately? I may have missed where this was documented.
An example for the target usage I am looking for:
- user launches an MCP server somewhere
python ./mcp_server.py
- user runs another script that connects to that server to perform a tool call. Below is the llama-index usage I am trying to implement
# this would connect to a server and enumerate the available tools
tool = MCPTool(url="127.0.0.1:8000")
# then we can plug that into an agent
agent = FunctionCallingAgent.from_tools([tool], llm=llm)
resp = agent.chat("Hey, use your tool")
If we can achieve something similar to the above, would love to add it to llama-index, but so far I haven't found a way to achieve the above using the current mcp package/docs.
I couldn't find an example of the SSE (Server-Sent Events) server either. However, the script below might help you get things set up.
Navigate to the directory examples/servers/simple-tool and save the code below in a file named client.py. From the same directory, run the script using the command:
uv run --script client.py
If the script hangs indefinitely, verify that the server starts successfully with the following command:
uv run mcp-simple-tool --transport sse --port 8000
# /// script
# requires-python = ">=3.10"
# dependencies = ["mcp"]
# [tool.uv.sources]
# mcp = { path = "../../..", editable = true }
# ///
import asyncio
import subprocess
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
def start_server():
# Start the server process
process = subprocess.Popen(
["uv", "run", "mcp-simple-tool", "--transport", "sse", "--port", "8000"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
for line in process.stderr:
if "Uvicorn running" in line:
print(line)
break
return process
def main():
# Start the server
server_process = start_server()
try:
# Run the client logic
asyncio.run(client_logic())
finally:
# Terminate the server process
server_process.terminate()
print("Server terminated.")
async def client_logic():
async with sse_client(url="http://0.0.0.0:8000/sse") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
print(tools)
# Call the fetch tool
result = await session.call_tool("fetch", {"url": "https://example.com"})
print(result)
if __name__ == "__main__":
main()
Or use fastmcp to run on the server. python server.py
#server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message.upper()}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"
if __name__ == "__main__":
mcp.run(transport='sse')
@logan-markewich I put up a working pattern for separate client and server processes with FastMCP still using SSE: https://github.com/sidharthrajaram/mcp-sse
Feel free to take a look.
I have developed web servers that integrate MCP SSE functionality:
- Using FastAPI: fastapi_mcp_sse
- Using Starlette: starlette_mcp_sse
These servers can be extended with custom routes while retaining full MCP SSE capabilities.
当前项目中的SseServerTransport._read_stream_writers 使用了一个dict保存MemoryObjectSendStream对象,而这个对象在sse模式下,需要在不同的request handler之间共享,这是一个非常不利于扩展的实现方式。
可以参考我设计的另一种Transport抽象逻辑:
https://github.com/modelcontextprotocol/python-sdk/issues/238#issuecomment-2720058579
There has been significant changes to the repo since this issue was last active including many more examples of how to use MCP. If this is still an issue please re-open the issue :)