Skip to content

Commit f5ac60e

Browse files
committed
updates for november release, prepping
1 parent fbd6513 commit f5ac60e

File tree

7 files changed

+59
-8
lines changed

7 files changed

+59
-8
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ Runs the same on macOS, Linux, and Windows — locally, serverside, and inside c
207207
| **LLM Providers** | OpenAI, Anthropic, Local models | Any LLM that outputs tool calls |
208208
| **MCP Transports** | HTTP Streamable, STDIO, SSE | All MCP 1.0 transports |
209209
| **MCP Servers** | Notion, SQLite, Atlassian, Echo, Custom | Any MCP-compliant server |
210+
| **MCP Specification** | 2025-11-25, 2025-06-18, 2025-03-26 | Full support via chuk-mcp 0.9 |
211+
212+
**MCP Protocol Support:**
213+
- ✅ MCP Spec versions: 2025-11-25 (November), 2025-06-18, 2025-03-26
214+
- ✅ Transports: HTTP Streamable, STDIO, SSE
215+
- ✅ Core operations: tools/call, tools/list, resources/list, resources/read, prompts/list, prompts/get
216+
- ✅ Icon metadata for tools, resources, and prompts (2025-11-25)
217+
- ✅ Structured content in tool results (2025-06-18)
218+
- ✅ OAuth 2.1 with PKCE and automatic token refresh
219+
- ✅ Session persistence and reconnection handling
220+
- ⏳ Tasks (experimental in 2025-11-25) - awaiting chuk-mcp support
221+
- ⏳ Sampling with tool invocation - server-side feature
210222

211223
**Tested Configurations:**
212224
- ✅ macOS 14+ (Apple Silicon & Intel)

src/chuk_tool_processor/mcp/register_mcp_tools.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ async def register_mcp_tools(
6868
"argument_schema": tool_def.get("inputSchema", {}),
6969
}
7070

71+
# Add icon if present (MCP spec 2025-11-25)
72+
if "icon" in tool_def:
73+
meta["icon"] = tool_def["icon"]
74+
7175
try:
7276
# Create MCPTool wrapper with optional resilience configuration
7377
wrapper = MCPTool(

src/chuk_tool_processor/mcp/stream_manager.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class StreamManager:
3636
Updated to support the latest transports:
3737
- STDIO (process-based)
3838
- SSE (Server-Sent Events) with headers support
39-
- HTTP Streamable (modern replacement for SSE, spec 2025-03-26) with graceful headers handling
39+
- HTTP Streamable (modern replacement for SSE, spec 2025-11-25) with graceful headers handling
4040
"""
4141

4242
def __init__(self, timeout_config: TimeoutConfig | None = None) -> None:
@@ -197,7 +197,7 @@ async def initialize(
197197
params, server_timeout = await load_config(config_file, server_name)
198198
# Use per-server timeout if specified, otherwise use global default
199199
effective_timeout = server_timeout if server_timeout is not None else default_timeout
200-
logger.info(
200+
logger.debug(
201201
f"Server '{server_name}' using timeout: {effective_timeout}s (per-server: {server_timeout}, default: {default_timeout})"
202202
)
203203
# Use initialization_timeout for connection_timeout since subprocess
@@ -250,6 +250,13 @@ async def initialize(
250250
session_id = None
251251
logger.debug("No URL configured for HTTP Streamable transport, using default: %s", http_url)
252252

253+
# IMPORTANT: If transport already exists for this server, preserve its session ID
254+
if server_name in self.transports:
255+
existing_transport = self.transports[server_name]
256+
if hasattr(existing_transport, "session_id") and existing_transport.session_id:
257+
session_id = existing_transport.session_id
258+
logger.debug(f"Preserving session ID for {server_name}: {session_id}")
259+
253260
# Build HTTP transport (headers not supported yet)
254261
transport_params = {
255262
"url": http_url,

src/chuk_tool_processor/mcp/transport/http_streamable_transport.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ async def _test_connection_health(self) -> bool:
145145

146146
async def initialize(self) -> bool:
147147
"""Initialize with enhanced error handling and health monitoring."""
148-
if self._initialized:
149-
logger.warning("Transport already initialized")
148+
if self._initialized and self._http_transport:
149+
logger.debug("Transport already initialized, reusing existing connection")
150150
return True
151151

152152
start_time = time.time()
@@ -164,11 +164,15 @@ async def initialize(self) -> bool:
164164

165165
# Create StreamableHTTPParameters with minimal configuration
166166
# NOTE: Keep params minimal - extra params can break message routing
167+
# IMPORTANT: Pass session_id if we have one from a previous connection
168+
if self.session_id:
169+
logger.debug(f"Creating transport with existing session ID: {self.session_id}")
167170
http_params = StreamableHTTPParameters(
168171
url=self.url,
169172
timeout=self.default_timeout,
170173
headers=headers,
171174
enable_streaming=True,
175+
session_id=self.session_id, # Reuse session ID if available
172176
)
173177

174178
# Create and store transport (will be managed via async with in parent scope)
@@ -214,6 +218,16 @@ async def initialize(self) -> bool:
214218
self._last_successful_ping = time.time()
215219
self._consecutive_failures = 0
216220

221+
# CRITICAL: Extract and persist session ID from transport
222+
# This allows stateful MCP servers to maintain user sessions across requests
223+
if self._http_transport:
224+
extracted_session_id = self._http_transport.get_session_id()
225+
print(f"DEBUG: Extracted session ID from transport: {extracted_session_id}")
226+
if extracted_session_id and extracted_session_id != self.session_id:
227+
self.session_id = extracted_session_id
228+
print(f"✓ Session ID established: {self.session_id}")
229+
logger.info(f"Session ID established: {self.session_id}")
230+
217231
total_init_time = time.time() - start_time
218232
if self.enable_metrics and self._metrics:
219233
self._metrics.initialization_time = total_init_time
@@ -294,10 +308,14 @@ async def close(self) -> None:
294308

295309
async def _cleanup(self) -> None:
296310
"""Enhanced cleanup with state reset."""
311+
# IMPORTANT: Preserve session_id across cleanup/reconnection
312+
# Session IDs must persist for the lifetime of the StreamableTransport object
313+
print(f"DEBUG: _cleanup() called on object {id(self)}, preserving session_id={self.session_id}")
297314
self._http_transport = None
298315
self._read_stream = None
299316
self._write_stream = None
300317
self._initialized = False
318+
# NOTE: We do NOT reset self.session_id here - it should persist across reconnections
301319

302320
async def send_ping(self) -> bool:
303321
"""Enhanced ping with health monitoring (like SSE)."""
@@ -471,6 +489,14 @@ async def call_tool(
471489
self._consecutive_failures = 0
472490
self._last_successful_ping = time.time() # Update health timestamp
473491

492+
# Extract and persist session ID after successful calls
493+
# This ensures we maintain session state even if it changes mid-session
494+
if self._http_transport:
495+
current_session_id = self._http_transport.get_session_id()
496+
if current_session_id and current_session_id != self.session_id:
497+
self.session_id = current_session_id
498+
logger.debug(f"Session ID updated: {self.session_id}")
499+
474500
if self.enable_metrics:
475501
self._update_metrics(response_time, not result.get("isError", False))
476502

src/chuk_tool_processor/mcp/transport/stdio_transport.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def _monitor_process_health(self) -> bool:
175175
async def initialize(self) -> bool:
176176
"""Enhanced initialization with process monitoring."""
177177
if self._initialized:
178-
logger.warning("Transport already initialized")
178+
logger.debug("Transport already initialized")
179179
return True
180180

181181
start_time = time.time()
@@ -252,7 +252,7 @@ async def _attempt_recovery(self) -> bool:
252252
self._metrics["recovery_attempts"] += 1
253253
self._metrics["process_restarts"] += 1
254254

255-
logger.warning("Attempting STDIO process recovery...")
255+
logger.debug("Attempting STDIO process recovery...")
256256

257257
try:
258258
# Force cleanup of existing process
@@ -374,7 +374,7 @@ def is_connected(self) -> bool:
374374

375375
# Check for too many consecutive failures (like SSE)
376376
if self._consecutive_failures >= self._max_consecutive_failures:
377-
logger.warning("Connection marked unhealthy after %d failures", self._consecutive_failures)
377+
logger.debug("Connection marked unhealthy after %d failures", self._consecutive_failures)
378378
return False
379379

380380
return True
@@ -467,7 +467,7 @@ async def call_tool(
467467

468468
# Enhanced connection check with recovery attempt
469469
if not self.is_connected():
470-
logger.warning("Connection unhealthy, attempting recovery...")
470+
logger.debug("Connection unhealthy, attempting recovery...")
471471
if not await self._attempt_recovery():
472472
if self.enable_metrics:
473473
self._update_metrics(time.time() - start_time, False)

src/chuk_tool_processor/models/tool_spec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ToolSpec(BaseModel):
7575
license: str | None = Field(None, description="License (e.g., 'MIT', 'Apache-2.0')")
7676
documentation_url: str | None = Field(None, description="Link to full documentation")
7777
source_url: str | None = Field(None, description="Link to source code")
78+
icon: str | None = Field(None, description="Icon URI or data URL for tool (MCP spec 2025-11-25)")
7879

7980
# Execution hints
8081
estimated_duration_seconds: float | None = Field(

tests/mcp/transport/test_http_streamable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ def test_init_with_session_id(self):
549549
async def test_initialize_already_initialized(self, transport):
550550
"""Test initialization when already initialized."""
551551
transport._initialized = True
552+
transport._http_transport = Mock() # Mock the transport object
552553
result = await transport.initialize()
553554
assert result is True
554555

0 commit comments

Comments
 (0)