Skip to content

Conversation

@beonde
Copy link
Member

@beonde beonde commented Jan 18, 2026

Summary

Completes the MCP SDK integration by implementing the CapiscioMCPServer and CapiscioMCPClient wrappers around the official MCP Python SDK's FastMCP class.

Changes

capiscio_mcp/integrations/mcp.py

  • CapiscioMCPServer: Now wraps FastMCP from official MCP SDK

    • run() method delegates to FastMCP.run() for stdio transport
    • tool() decorator wires to FastMCP.tool() with @guard enforcement
    • identity_meta property exposes server DID and badge for initialize response
    • create_initialize_response_meta() handles PoP signing
  • CapiscioMCPClient: Supports stdio transport via MCP SDK

    • connect() spawns server process and creates MCP session
    • close() properly cleans up resources
    • call_tool() delegates to MCP client session
    • list_tools() returns available tools from server
    • create_initialize_request_meta() generates PoP nonce

README.md

  • Added MCP SDK Integration section with working examples
  • Server example: CapiscioMCPServer + @server.tool(min_trust_level=2)
  • Client example: CapiscioMCPClient(command="python", args=[...])
  • Updated API Reference with MCP SDK integration classes

capiscio_mcp/__init__.py

  • Fixed exports for MCP integration availability check

Usage Example

# Server
from capiscio_mcp.integrations.mcp import CapiscioMCPServer

server = CapiscioMCPServer(
    name="my-server",
    did="did:web:example.com:servers:my-server",
)

@server.tool(min_trust_level=2)
async def secure_action(data: str) -> str:
    return f"Processed: {data}"

server.run()  # Runs over stdio
# Client
from capiscio_mcp.integrations.mcp import CapiscioMCPClient

async with CapiscioMCPClient(
    command="python",
    args=["my_mcp_server.py"],
    min_trust_level=1,
) as client:
    tools = await client.list_tools()
    result = await client.call_tool("secure_action", {"data": "test"})

Testing

  • All 19 MCP stdio E2E tests pass
  • 98 total MCP tests pass across all test files

Related

  • E2E tests: capiscio/capiscio-e2e-tests#feature/mcp-e2e-tests

- 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
Copilot AI review requested due to automatic review settings January 18, 2026 04:01
Copy link

Copilot AI left a 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 CapiscioMCPServer wrapping FastMCP with tool guarding and identity disclosure
  • Implements CapiscioMCPClient with 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.

Comment on lines 178 to 190
### 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:
Copy link

Copilot AI Jan 18, 2026

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.

Copilot uses AI. Check for mistakes.
api_key: Client API key for authentication (alternative)
"""
_require_mcp_client()

Copy link

Copilot AI Jan 18, 2026

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.

Suggested change
# 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."
)

Copilot uses AI. Check for mistakes.
Comment on lines 356 to 385
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")
Copy link

Copilot AI Jan 18, 2026

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.

Copilot uses AI. Check for mistakes.
README.md Outdated
Comment on lines 356 to 359
- `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

Copy link

Copilot AI Jan 18, 2026

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.

Suggested change
- `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.*

Copilot uses AI. Check for mistakes.
Comment on lines 597 to 609
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()
Copy link

Copilot AI Jan 18, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 704 to 705
# Note: credential context is thread-local, no explicit reset needed
pass
Copy link

Copilot AI Jan 18, 2026

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.

Suggested change
# Note: credential context is thread-local, no explicit reset needed
pass
# Reset credential context to avoid leakage between calls/tasks
_current_credential.reset(token)

Copilot uses AI. Check for mistakes.
Comment on lines 562 to 640
# 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}")
Copy link

Copilot AI Jan 18, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +429 to +431
server_url: Optional[str] = None,
command: Optional[str] = None,
args: Optional[List[str]] = None,
Copy link

Copilot AI Jan 18, 2026

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.

Copilot uses AI. Check for mistakes.
- 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
@github-actions
Copy link

✅ Integration tests passed! capiscio-core gRPC tests working.

@beonde beonde merged commit c5ca83f into main Jan 18, 2026
10 checks passed
@beonde beonde deleted the feature/mcp-sdk-wrapper branch January 18, 2026 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants