-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Complete MCP SDK integration with FastMCP #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Update CapiscioMCPServer to wrap FastMCP from official MCP SDK - Implement run() method delegating to FastMCP.run() - Wire tool() decorator to FastMCP with @guard enforcement - Implement CapiscioMCPClient with stdio transport support - Add connect(), close(), call_tool(), list_tools() methods - Update README with working MCP SDK integration examples - Fix __init__.py exports for MCP integrations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR completes the MCP SDK integration by implementing CapiscioMCPServer and CapiscioMCPClient wrappers around the official MCP Python SDK's FastMCP class and client components. The implementation provides server-side tool guarding with trust level enforcement and client-side server verification capabilities.
Changes:
- Implements
CapiscioMCPServerwrapping FastMCP with tool guarding and identity disclosure - Implements
CapiscioMCPClientwith stdio transport support for connecting to MCP servers - Updates README with comprehensive MCP SDK integration documentation and examples
- Exports new utility functions (
TrustLevel,evaluate_tool_access,verify_server_strict) in__init__.py
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| capiscio_mcp/integrations/mcp.py | Implements CapiscioMCPServer using FastMCP and CapiscioMCPClient with stdio transport; server identity disclosure and client verification framework |
| capiscio_mcp/init.py | Adds exports for TrustLevel enum and new utility functions |
| README.md | Adds MCP SDK Integration section with server/client examples and API reference updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### Client with Trust Verification | ||
|
|
||
| Connect to MCP servers with automatic identity verification: | ||
|
|
||
| ```python | ||
| from capiscio_mcp.integrations.mcp import CapiscioMCPClient | ||
|
|
||
| async with CapiscioMCPClient( | ||
| command="python", | ||
| args=["my_mcp_server.py"], | ||
| min_trust_level=1, | ||
| badge="eyJhbGc...", # Your client badge | ||
| ) as client: |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation claims "automatic identity verification" but this feature is not implemented in the current code. As noted in the implementation comments (lines 617-618), the MCP SDK doesn't expose _meta from initialize responses, so verification is always skipped. The README should clarify this limitation or remove claims of automatic verification to avoid misleading users.
| api_key: Client API key for authentication (alternative) | ||
| """ | ||
| _require_mcp_client() | ||
|
|
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no validation that either server_url or command is provided. Both parameters are Optional, and both can be None simultaneously. This will cause a confusing error later when connect() is called - it will raise NotImplementedError for HTTP transport. Consider adding validation in init to ensure at least one transport method is specified.
| # Ensure that at least one transport method is configured | |
| if server_url is None and command is None: | |
| raise ValueError( | |
| "Either server_url or command must be provided to CapiscioMCPClient " | |
| "to select an HTTP or stdio transport." | |
| ) |
| async def run_stdio(self) -> None: | ||
| """Run the server over stdio transport.""" | ||
| # Implementation depends on MCP SDK | ||
| pass | ||
| """Run the server over stdio transport (async version).""" | ||
| # For backwards compatibility, delegate to run() | ||
| self._server.run(transport="stdio") | ||
|
|
||
| async def run_sse(self, port: int = 8080) -> None: | ||
| """Run the server over SSE transport.""" | ||
| # Implementation depends on MCP SDK | ||
| pass | ||
| """Run the server over SSE transport (deprecated, use streamable-http).""" | ||
| logger.warning("SSE transport is deprecated, use streamable-http instead") | ||
| self._server.run(transport="sse") |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The run_stdio and run_sse methods are declared as async but call the synchronous self._server.run() method. This creates an async function that doesn't await anything, which is confusing and could lead to unexpected behavior. Either make these methods synchronous (def instead of async def) or properly await if FastMCP.run() is actually async.
README.md
Outdated
| - `CapiscioMCPClient(command, args, min_trust_level, ...)` — Client with verification | ||
| - `CapiscioMCPClient.call_tool(name, args)` — Call a tool on verified server | ||
| - `CapiscioMCPClient.list_tools()` — List available tools | ||
|
|
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API reference claims the CapiscioMCPClient provides "Client with verification" but the implementation shows that verification is not actually working. Lines 619-620 in the implementation show server_did and server_badge are always None, and the comment on lines 617-618 states "MCP SDK currently doesn't expose _meta from initialize response easily." The documentation should clarify that verification is currently not functional.
| - `CapiscioMCPClient(command, args, min_trust_level, ...)` — Client with verification | |
| - `CapiscioMCPClient.call_tool(name, args)` — Call a tool on verified server | |
| - `CapiscioMCPClient.list_tools()` — List available tools | |
| - `CapiscioMCPClient(command, args, min_trust_level, ...)` — Client with planned verification support* | |
| - `CapiscioMCPClient.call_tool(name, args)` — Call a tool on the server | |
| - `CapiscioMCPClient.list_tools()` — List available tools | |
| *Note: Due to current MCP SDK limitations (no easy access to `_meta` in the initialize response), server identity verification and trust badge checks in `CapiscioMCPClient` are not yet fully functional and should be treated as experimental.* |
capiscio_mcp/integrations/mcp.py
Outdated
| if self.command: | ||
| # Stdio transport - spawn server process | ||
| server_params = StdioServerParameters( | ||
| command=self.command, | ||
| args=self.args, | ||
| ) | ||
| self._context_manager = stdio_client(server_params) | ||
| read_stream, write_stream = await self._context_manager.__aenter__() | ||
| self._session = McpClientSession(read_stream, write_stream) | ||
| await self._session.__aenter__() | ||
|
|
||
| # Initialize the session | ||
| await self._session.initialize() |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an exception occurs after self._context_manager.aenter() is called (line 604) but before connect() completes, the context manager won't be properly cleaned up. For example, if self._session.aenter() fails on line 606, the stdio_client context manager will remain open. Consider wrapping the initialization in try-except to ensure proper cleanup on failure.
capiscio_mcp/integrations/mcp.py
Outdated
| # Note: credential context is thread-local, no explicit reset needed | ||
| pass |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The token returned by set_credential is not being used to reset the credential context. According to the set_credential documentation, the token should be used to reset the credential via _current_credential.reset(token). The current implementation with an empty pass in the finally block doesn't properly clean up the context variable, which could lead to credential leakage between different tool calls or async tasks.
| # Note: credential context is thread-local, no explicit reset needed | |
| pass | |
| # Reset credential context to avoid leakage between calls/tasks | |
| _current_credential.reset(token) |
| # Extract server identity from initialize response | ||
| # This is a placeholder - actual implementation depends on MCP SDK | ||
| # Note: MCP SDK currently doesn't expose _meta from initialize response easily | ||
| # This is a known limitation - identity verification works via separate channels | ||
| server_did: Optional[str] = None | ||
| server_badge: Optional[str] = None | ||
|
|
||
| # If we get _meta from initialize response: | ||
| # server_did, server_badge = parse_jsonrpc_meta(init_result.meta) | ||
|
|
||
| # Verify server identity | ||
| self._verify_result = await verify_server( | ||
| server_did=server_did, | ||
| server_badge=server_badge, | ||
| transport_origin=self.server_url, | ||
| config=self.verify_config, | ||
| ) | ||
|
|
||
| # Enforce requirements | ||
| if self.fail_on_unverified and self._verify_result.state == ServerState.UNVERIFIED_ORIGIN: | ||
| raise ServerVerifyError( | ||
| error_code=self._verify_result.error_code, | ||
| detail=f"Server at {self.server_url} did not disclose identity", | ||
| state=self._verify_result.state, | ||
| ) | ||
|
|
||
| if self._verify_result.state == ServerState.DECLARED_PRINCIPAL and self.min_trust_level > 0: | ||
| raise ServerVerifyError( | ||
| error_code=self._verify_result.error_code, | ||
| detail=f"Server at {self.server_url} did not provide verifiable badge", | ||
| state=self._verify_result.state, | ||
| server_did=self._verify_result.server_did, | ||
| ) | ||
|
|
||
| if ( | ||
| self._verify_result.state == ServerState.VERIFIED_PRINCIPAL | ||
| and self._verify_result.trust_level is not None | ||
| and self._verify_result.trust_level < self.min_trust_level | ||
| ): | ||
| raise ServerVerifyError( | ||
| error_code=self._verify_result.error_code, | ||
| detail=( | ||
| f"Server trust level {self._verify_result.trust_level} " | ||
| f"is below required {self.min_trust_level}" | ||
| ), | ||
| state=self._verify_result.state, | ||
| server_did=self._verify_result.server_did, | ||
| # For now, we skip verification if we can't get identity | ||
| # Full verification requires protocol support for _meta passthrough | ||
| if server_did or server_badge: | ||
| self._verify_result = await verify_server( | ||
| server_did=server_did, | ||
| server_badge=server_badge, | ||
| transport_origin=self.server_url or f"stdio:{self.command}", | ||
| config=self.verify_config, | ||
| ) | ||
|
|
||
| # Enforce requirements | ||
| if self.fail_on_unverified and self._verify_result.state == ServerState.UNVERIFIED_ORIGIN: | ||
| raise ServerVerifyError( | ||
| error_code=self._verify_result.error_code, | ||
| detail=f"Server did not disclose identity", | ||
| state=self._verify_result.state, | ||
| ) | ||
|
|
||
| logger.info( | ||
| f"Connected to {self.server_url}: " | ||
| f"state={self._verify_result.state.value}, " | ||
| f"trust_level={self._verify_result.trust_level}" | ||
| ) | ||
| logger.info(f"Connected to MCP server: {self.command or self.server_url}") |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The identity verification is always skipped because server_did and server_badge are hardcoded to None and never populated from the initialize response. The comment mentions this is a known limitation of the MCP SDK, but this means the client's fail_on_unverified and min_trust_level parameters have no effect. This should be clearly documented in the method docstring to avoid misleading users about the security guarantees.
| server_url: Optional[str] = None, | ||
| command: Optional[str] = None, | ||
| args: Optional[List[str]] = None, |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server_url parameter is now Optional but is still used in error messages and verification calls without null checks. When using stdio transport with command/args, server_url will be None. Line 628 attempts to handle this with the expression self.server_url or f"stdio:{self.command}", but this will only work when server_did or server_badge are set (which they never are based on lines 619-620). Consider adding validation to ensure either server_url or command is provided.
- Update README to clarify verification limitations (not automatic) - Add validation: require server_url OR command in CapiscioMCPClient - Fix run_stdio/run_sse to be sync methods (not async) - Add proper cleanup in connect() on failure - Fix credential reset with _current_credential.reset(token) - Document verification limitations in connect() docstring - Update tests to mock FastMCP instead of McpServer - Update client tests to use command parameter where applicable
|
✅ Integration tests passed! capiscio-core gRPC tests working. |
Summary
Completes the MCP SDK integration by implementing the
CapiscioMCPServerandCapiscioMCPClientwrappers around the official MCP Python SDK'sFastMCPclass.Changes
capiscio_mcp/integrations/mcp.pyCapiscioMCPServer: Now wraps
FastMCPfrom official MCP SDKrun()method delegates toFastMCP.run()for stdio transporttool()decorator wires toFastMCP.tool()with@guardenforcementidentity_metaproperty exposes server DID and badge for initialize responsecreate_initialize_response_meta()handles PoP signingCapiscioMCPClient: Supports stdio transport via MCP SDK
connect()spawns server process and creates MCP sessionclose()properly cleans up resourcescall_tool()delegates to MCP client sessionlist_tools()returns available tools from servercreate_initialize_request_meta()generates PoP nonceREADME.mdCapiscioMCPServer+@server.tool(min_trust_level=2)CapiscioMCPClient(command="python", args=[...])capiscio_mcp/__init__.pyUsage Example
Testing
Related