Skip to content

Commit 9e0da0c

Browse files
committed
Add support for protocol 2025-06-18
1 parent 634be9f commit 9e0da0c

File tree

4 files changed

+104
-84
lines changed

4 files changed

+104
-84
lines changed

README.md

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ A Python utility package for building Model Context Protocol (MCP) servers.
1717
- [Optional Dependencies](#optional-dependencies)
1818
- [Usage](#usage)
1919
- [Basic MCP Server](#basic-mcp-server)
20-
- [Flask with Redis Example](#flask-with-redis-example)
20+
- [Flask Example](#flask-example)
2121
- [SQLAlchemy Transaction Handling Example](#sqlalchemy-transaction-handling-example)
22+
- [Running with Gunicorn](#running-with-gunicorn)
2223
- [Connecting with MCP Clients](#connecting-with-mcp-clients)
24+
- [Cursor](#cursor)
2325
- [Claude Desktop](#claude-desktop)
2426
- [Installing via Smithery](#installing-via-smithery)
2527
- [Installing via PyPI](#installing-via-pypi)
@@ -94,40 +96,27 @@ def get_weather(city: str) -> str:
9496
return "sunny"
9597
```
9698

97-
### Flask with Redis Example
99+
### Flask Example
100+
101+
For production use, you can use a simple Flask app with the mcp server and
102+
support [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)
103+
from version 2025-06-18.
98104

99-
For production use, you can integrate the MCP server with Flask and Redis for better message handling:
100105

101106
```python
102107
from flask import Flask, Response, url_for, request
103-
import redis
104-
from mcp_utils.queue import RedisResponseQueue
105-
106-
# Setup Redis client
107-
redis_client = redis.Redis(host="localhost", port=6379, db=0)
108108

109109
# Create Flask app and MCP server with Redis queue
110110
app = Flask(__name__)
111111
mcp = MCPServer(
112112
"example",
113113
"1.0",
114-
response_queue=RedisResponseQueue(redis_client)
115114
)
116115

117-
@app.route("/sse")
118-
def sse():
119-
session_id = mcp.generate_session_id()
120-
messages_endpoint = url_for("message", session_id=session_id)
121-
return Response(
122-
mcp.sse_stream(session_id, messages_endpoint),
123-
mimetype="text/event-stream"
124-
)
125-
126-
127-
@app.route("/message/<session_id>", methods=["POST"])
128-
def message(session_id):
129-
mcp.handle_message(session_id, request.get_json())
130-
return "", 202
116+
@app.route("/mcp", methods=["POST"])
117+
def mcp_route():
118+
response = mcp.handle_message(request.get_json())
119+
return jsonify(response.model_dump(exclude_none=True))
131120

132121

133122
if __name__ == "__main__":
@@ -142,11 +131,6 @@ For production use, you can integrate the MCP server with Flask, Redis, and SQLA
142131
from flask import Flask, request
143132
from sqlalchemy.orm import Session
144133
from sqlalchemy import create_engine
145-
import redis
146-
from mcp_utils.queue import RedisResponseQueue
147-
148-
# Setup Redis client
149-
redis_client = redis.Redis(host="localhost", port=6379, db=0)
150134

151135
# Create engine for PostgreSQL database
152136
engine = create_engine("postgresql://user:pass@localhost/dbname")
@@ -156,31 +140,86 @@ app = Flask(__name__)
156140
mcp = MCPServer(
157141
"example",
158142
"1.0",
159-
response_queue=RedisResponseQueue(redis_client)
160143
)
161144

162-
@app.route("/message/<session_id>", methods=["POST"])
163-
def message(session_id):
145+
@app.route("/mcp", methods=["POST"])
146+
def mcp_route():
164147
with Session(engine) as session:
165148
try:
166-
mcp.handle_message(session_id, request.get_json())
149+
response = mcp.handle_message(request.get_json())
167150
session.commit()
168-
return "", 202
169-
except Exception as e:
151+
except:
170152
session.rollback()
171153
raise
154+
else:
155+
return jsonify(response.model_dump(exclude_none=True))
156+
172157

173158
if __name__ == "__main__":
174159
app.run(debug=True)
175160
```
176161

177162
For a more comprehensive example including logging setup and session management, check out the [example Flask application](https://github.com/fulfilio/mcp-utils/blob/main/examples/flask_app.py) in the repository.
178163

164+
### Running with Gunicorn
165+
166+
Gunicorn is a better approach to running even locally. To run the app with gunicorn
167+
168+
```python
169+
from gunicorn.app.base import BaseApplication
170+
171+
class FlaskApplication(BaseApplication):
172+
def __init__(self, app, options=None):
173+
self.options = options or {}
174+
self.application = app
175+
super().__init__()
176+
177+
def load_config(self):
178+
config = {
179+
key: value
180+
for key, value in self.options.items()
181+
if key in self.cfg.settings
182+
}
183+
for key, value in config.items():
184+
self.cfg.set(key.lower(), value)
185+
186+
def load(self):
187+
return self.application
188+
189+
190+
if __name__ == "__main__":
191+
handler = logging.StreamHandler(sys.stdout)
192+
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
193+
handler.setFormatter(formatter)
194+
logger.addHandler(handler)
195+
options = {
196+
"bind": "0.0.0.0:9000",
197+
"workers": 1,
198+
"worker_class": "gevent",
199+
"loglevel": "debug",
200+
}
201+
FlaskApplication(app, options).run()
202+
```
203+
179204
## Connecting with MCP Clients
180205

206+
### Cursor
207+
208+
* Edit MCP settings and add to configuration
209+
210+
```json
211+
{
212+
"mcpServers": {
213+
"server-name": {
214+
"url": "http://localhost:9000/mcp"
215+
}
216+
}
217+
}
218+
```
219+
181220
### Claude Desktop
182221

183-
Currently, only Claude Desktop (not claude.ai) can connect to MCP servers. As of this writing, Claude Desktop does not support MCP through SSE and only supports stdio. To connect Claude Desktop with an MCP server, you'll need to use [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy).
222+
As of this writing, Claude Desktop does not support MCP through SSE and only supports stdio. To connect Claude Desktop with an MCP server, you'll need to use [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy).
184223

185224
Configuration example for Claude Desktop:
186225

examples/flask_app.py

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import sys
1818

1919
import redis
20-
from flask import Flask, Response, request, url_for
20+
from flask import Flask, Response, jsonify, request, url_for
2121
from gunicorn.app.base import BaseApplication
2222

2323
from mcp_utils.core import MCPServer
@@ -30,14 +30,9 @@
3030
TextContent,
3131
)
3232

33-
redis_client = redis.Redis(
34-
host="localhost",
35-
port=6379,
36-
db=0,
37-
)
3833

3934
app = Flask(__name__)
40-
mcp = MCPServer("weather", "1.0", response_queue=RedisResponseQueue(redis_client))
35+
mcp = MCPServer("weather", "1.0")
4136

4237
logger = logging.getLogger("mcp_utils")
4338
logger.setLevel(logging.DEBUG)
@@ -85,27 +80,10 @@ def get_cities(city_name: str) -> CompletionValues:
8580
return [city for city in all_cities if city.lower().startswith(city_name.lower())]
8681

8782

88-
@app.route("/sse")
89-
def sse():
90-
session_id = mcp.generate_session_id()
91-
messages_endpoint = url_for("message", session_id=session_id)
92-
logger.info(f"SSE endpoint: {messages_endpoint}")
93-
logger.info(f"Session ID: {session_id}")
94-
95-
return Response(
96-
mcp.sse_stream(session_id, messages_endpoint), mimetype="text/event-stream"
97-
)
98-
99-
100-
@app.route("/message", methods=["POST"])
101-
def message():
102-
"""
103-
Messages from the client are received as a POST
104-
request with a JSON body.
105-
"""
106-
logger.debug(f"Received message: {request.get_json()}")
107-
mcp.handle_message(request.args["session_id"], request.get_json())
108-
return "", 202
83+
@app.route("/mcp", methods=["POST"])
84+
def mcp_route():
85+
response = mcp.handle_message(request.get_json())
86+
return jsonify(response.model_dump(exclude_none=True))
10987

11088

11189
class FlaskApplication(BaseApplication):

src/mcp_utils/core.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ class MCPServer:
5454
# by the server.
5555
# The client is responsible to disconnect if the version is
5656
# not supported.
57-
# https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#version-negotiation
58-
protocol_version: str = "2024-11-05"
57+
# https://spec.modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle/#version-negotiation
58+
protocol_version: str = "2025-06-18"
5959

6060
# server capabilities can be:
6161
# - prompts
@@ -200,7 +200,7 @@ def generate_session_id(self) -> str:
200200
"""
201201
return str(uuid.uuid4())
202202

203-
def sse_stream(self, session_id: str, messages_endpoint: str | None = None):
203+
def sse_stream(self, session_id: str, messages_endpoint: str):
204204
"""
205205
Create a Server-Sent Events (SSE) stream for a session
206206
@@ -211,11 +211,10 @@ def sse_stream(self, session_id: str, messages_endpoint: str | None = None):
211211
Returns:
212212
Iterator yielding SSE formatted strings
213213
"""
214-
if messages_endpoint is not None:
215-
# The first message is the endpoint itself
216-
endpoint_response = f"event: endpoint\ndata: {messages_endpoint}\n\n"
217-
logger.debug(f"Sending endpoint: {endpoint_response}")
218-
yield endpoint_response
214+
# The first message is the endpoint itself
215+
endpoint_response = f"event: endpoint\ndata: {messages_endpoint}\n\n"
216+
logger.debug(f"Sending endpoint: {endpoint_response}")
217+
yield endpoint_response
219218

220219
# Now loop and block forever and keep yielding responses
221220
try:
@@ -484,16 +483,20 @@ def _handle_tools_call(self, request: MCPRequest) -> MCPResponse:
484483
),
485484
)
486485

487-
def handle_message(self, session_id: str, message: dict) -> MCPResponse | None:
486+
def handle_message(
487+
self, message: dict, session_id: str | None = None
488+
) -> MCPResponse | None:
488489
"""Handle incoming MCP messages."""
489490
logger.debug(f"Handling message: {message}")
490-
response = self._handle_message(session_id, message)
491+
response = self._handle_message(message, session_id)
491492
logger.debug(f"Response: {response}")
492-
if response is not None:
493+
if response is not None and session_id is not None:
493494
self.response_queue.push_response(session_id, response)
494495
return response
495496

496-
def _handle_message(self, session_id: str, message: dict) -> MCPResponse | None:
497+
def _handle_message(
498+
self, message: dict, session_id: str | None = None
499+
) -> MCPResponse | None:
497500
try:
498501
message_id = message["id"]
499502
except KeyError:

tests/test_core.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def test_message_handling(server: MCPServer) -> None:
307307
"clientInfo": {"name": "Test Client", "version": "1.0.0"},
308308
},
309309
}
310-
server.handle_message(session_id, init_request)
310+
server.handle_message(message=init_request, session_id=session_id)
311311
response = json.loads(server.wait_for_queued_response(session_id))
312312
assert response["jsonrpc"] == "2.0"
313313
assert response["id"] == "1"
@@ -326,7 +326,7 @@ def echo(message: str) -> dict[str, str]:
326326
"method": "tools/call",
327327
"params": {"name": "echo", "arguments": {"message": "hello"}},
328328
}
329-
server.handle_message(session_id, tool_request)
329+
server.handle_message(tool_request, session_id)
330330
response = json.loads(server.wait_for_queued_response(session_id))
331331
assert response["jsonrpc"] == "2.0"
332332
assert response["id"] == "2"
@@ -344,7 +344,7 @@ def echo_str(message: str) -> str:
344344
"method": "tools/call",
345345
"params": {"name": "echo_str", "arguments": {"message": "hello"}},
346346
}
347-
server.handle_message(session_id, tool_request)
347+
server.handle_message(tool_request, session_id)
348348
response = json.loads(server.wait_for_queued_response(session_id))
349349
assert response["jsonrpc"] == "2.0"
350350
assert response["id"] == "2"
@@ -362,7 +362,7 @@ def echo_dict(message: str) -> dict:
362362
"method": "tools/call",
363363
"params": {"name": "echo_dict", "arguments": {"message": "hello"}},
364364
}
365-
server.handle_message(session_id, tool_request)
365+
server.handle_message(tool_request, session_id)
366366
response = json.loads(server.wait_for_queued_response(session_id))
367367
assert response["jsonrpc"] == "2.0"
368368
assert response["id"] == "3"
@@ -380,7 +380,7 @@ def echo_result(message: str) -> CallToolResult:
380380
"method": "tools/call",
381381
"params": {"name": "echo_result", "arguments": {"message": "hello"}},
382382
}
383-
server.handle_message(session_id, tool_request)
383+
server.handle_message(tool_request, session_id)
384384
response = json.loads(server.wait_for_queued_response(session_id))
385385
assert response["jsonrpc"] == "2.0"
386386
assert response["id"] == "4"
@@ -393,7 +393,7 @@ def test_error_handling(server: MCPServer) -> None:
393393

394394
# Test invalid message format
395395
invalid_message = {"not": "valid"}
396-
server.handle_message(session_id, invalid_message)
396+
server.handle_message(invalid_message, session_id)
397397
response = json.loads(server.wait_for_queued_response(session_id))
398398
assert response["error"]["code"] == -32600 # Invalid Request
399399

@@ -404,7 +404,7 @@ def test_error_handling(server: MCPServer) -> None:
404404
"method": "unknown",
405405
"params": {},
406406
}
407-
server.handle_message(session_id, unknown_method)
407+
server.handle_message(unknown_method, session_id)
408408
response = json.loads(server.wait_for_queued_response(session_id))
409409
assert response["error"]["code"] == -32601 # Method not found
410410

@@ -415,7 +415,7 @@ def test_error_handling(server: MCPServer) -> None:
415415
"method": "tools/call",
416416
"params": {"name": "nonexistent", "arguments": {}},
417417
}
418-
server.handle_message(session_id, invalid_tool)
418+
server.handle_message(invalid_tool, session_id)
419419
response = json.loads(server.wait_for_queued_response(session_id))
420420
assert response["error"]["code"] == -32601 # Method not found
421421

@@ -431,7 +431,7 @@ def test_tool(message: str) -> None:
431431
"method": "tools/call",
432432
"params": {"name": "test_tool", "arguments": {"wrong_arg": "value"}},
433433
}
434-
server.handle_message(session_id, invalid_args)
434+
server.handle_message(invalid_args, session_id)
435435
response = json.loads(server.wait_for_queued_response(session_id))
436436
assert response["error"]["code"] == -32602 # Invalid params
437437

0 commit comments

Comments
 (0)