-
Notifications
You must be signed in to change notification settings - Fork 488
feat(server): MCP server with actions as tools #4012
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
Your mrge subscription is currently inactive. Please reactivate your subscription to receive AI reviews and use mrge. |
.setTag('nango.providerConfigKey', providerConfig.unique_key); | ||
|
||
const logCtx = await logContextGetter.create( | ||
{ operation: { type: 'action', action: 'run' }, expiresAt: defaultOperationExpiration.action() }, |
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.
Perhaps we add something to indicate this comes from MCP?
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.
we'll probably need at some point. I think it is ok without it for now
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.
Thank you. my main concern is about creating a mcp server for each instance
@@ -172,7 +172,7 @@ class ConfigController { | |||
}; | |||
}); | |||
|
|||
const actions = await getActionsByProviderConfigKey(environmentId, providerConfigKey); | |||
const actions = await getSimplifiedActionsByProviderConfigKey(environmentId, providerConfigKey); |
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.
I don't really follow why this change is about?
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 an existing getActionsByProviderConfigKey
which is exactly what I need, except it strips everything and just leaves the sync_name (renamed as name
). I didn't want to modify it because it's used in some payloads and I want to avoiding breaking anything unrelated. But it's annoying because it takes exactly the function name that I needed.
I could name my function something else, but I would expect getActionsByProviderConfigKey
to give me the full action, not a modified and stripped down version of it, so I decided to rename the existing function instead.
res.status(500).json({ | ||
jsonrpc: '2.0', | ||
error: { | ||
code: -32603, |
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.
what does this code come from?
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 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.
can we add a comment with this link to easily find it in the future?
jsonrpc: '2.0', | ||
error: { | ||
code: -32603, | ||
message: 'Internal server error' |
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.
should we return more details about the error?
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.
Replaced by the Result pattern and added context.
} | ||
}); | ||
|
||
// We have to be explicit about not supporting SSE |
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.
is that part of the protocol?
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.
Yes. The client may attempt to open an SSE stream on this endpoint, and we have to respond.
import type { Span } from 'dd-trace'; | ||
import type { JSONSchema7 } from 'json-schema'; | ||
|
||
export async function createMcpServerForConnection(account: DBTeam, environment: DBEnvironment, connection: DBConnectionDecrypted, providerConfigKey: string) { |
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.
all new functions should return a Result
. It makes error handling more explicit
} | ||
|
||
try { | ||
const server = await createMcpServerForConnection(account, environment, connection, providerConfigKey); |
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.
why create a mcp server instance on each request instead of having one mcp server with logic to fetch actions in the handlers?
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.
Mostly following their advice on their example: https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#without-session-management-stateless
// In stateless mode, create a new instance of transport and server for each request
// to ensure complete isolation. A single instance would cause request ID collisions
// when multiple clients connect concurrently.
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.
I am not sold yet on this recommendations but let's see. Good for me for now
|
||
server.setRequestHandler(ListToolsRequestSchema, () => { | ||
return { | ||
tools: actions.map(actionToTool).filter((tool) => tool !== null) |
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.
you can flatMap instead of map/filter
.setTag('nango.providerConfigKey', providerConfig.unique_key); | ||
|
||
const logCtx = await logContextGetter.create( | ||
{ operation: { type: 'action', action: 'run' }, expiresAt: defaultOperationExpiration.action() }, |
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.
we'll probably need at some point. I think it is ok without it for now
|
||
if (error || !connection) { | ||
res.status(400).send({ | ||
error: { code: 'unknown_connection', message: 'Provided ConnectionId and ProviderConfigKey does not match a valid connection' } |
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.
error: { code: 'unknown_connection', message: 'Provided ConnectionId and ProviderConfigKey does not match a valid connection' } | |
error: { code: 'unknown_connection', message: 'Provided ConnectionId and ProviderConfigKey do not match a valid connection.' } |
tip: I find llms are pretty good at copy-writing and fixing my unperfect english
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.
From a usage perspective, tested and works well! Nice work
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.
works well with my dummy actions locally.
Please rebase with master and address the comments (especially about logging for actions that doesn't have to be done on server).
Otherwise you are good to go
Description
This PR adds basic MCP support for executing action. It's basic support for the latest "Streamable Http", and implemented in a stateless manner (based on their example).
This means every message creates a new instance of an
McpServer
andStreamableHttpServerTransport
. This way no connection management is required and it should be scalable out of the box.Any given call requires a
connection-id
. The tools available to the caller are the respective active actions of that connection-id.Testing
I'll be creating a repo with a working example of a client implementation using vercel's AI SDK.
Otherwise, an easy way to test is by installing Claude for Desktop and setting up your mcp servers on:
/Users/<your-user>/Library/Application Support/Claude/claude_desktop_config.json
with this format:MCP Server Implementation with Actions as Tools
This PR adds Model Context Protocol (MCP) support to Nango's server, allowing AI assistants to discover and execute Nango actions as tools. The implementation follows a stateless approach where each request creates new server instances, avoiding shared state issues. Actions are exposed as MCP-compatible tools with their schemas automatically converted from Nango's format, enabling seamless interaction between AI models and Nango integrations.
Key Changes:
• Added new /mcp endpoint with authentication and connection validation
• Implemented stateless
MCP
server that maps Nango actions toMCP
tools• Created schema conversion logic to transform action inputs to
MCP
-compatible formats• Added action execution handler that processes tool calls and returns structured results
Affected Areas:
• Server
API
endpoints and routing• Action retrieval and execution flow
• Server dependencies (added @modelcontextprotocol/sdk)
• Type definitions for
MCP
requests/responsesPotential Impact:
Functionality: Enables AI assistants to discover and execute Nango actions through the MCP protocol, expanding integration capabilities for LLM-based applications
Performance: Minimal impact as the implementation follows a stateless pattern that creates isolated server instances per request, avoiding shared state issues
Security: Leverages existing authentication mechanisms and validation for connections, maintaining security boundaries
Scalability: The stateless approach should scale well by avoiding session management overhead
Review Focus:
• Stateless implementation approach for handling
MCP
requests• Schema conversion logic between Nango actions and
MCP
tools• Error handling and compliance with
MCP
protocol specifications• Response formatting and result serialization
Testing Needed
• Test with Claude desktop client to verify discovery and execution of tools
• Verify schema conversion handles complex action input types correctly
• Test error cases (invalid connections, actions, authentication)
• Test concurrent requests to ensure isolated handling
Code Quality Assessment
packages/server/lib/controllers/mcp/server.ts: Well-structured implementation with proper error handling using Result pattern. Good separation of concerns between tool conversion, request handling, and execution.
packages/server/lib/controllers/mcp/mcp.ts: Clean request validation and handler implementation. Follows MCP protocol specifications for response formatting.
Best Practices
Error Handling:
• Uses Result pattern for robust error management
• Validates inputs before processing
Security:
• Leverages existing authentication mechanisms
• Validates connections before exposing actions
Code Organization:
• Clear separation between protocol handling and business logic
• Type-safe implementation
Possible Issues
• Complex action schemas might not convert perfectly to MCP tool schemas
• Error responses might need refinement as MCP protocol evolves
• Each request creates new server instances which could be optimized in the future
This summary was automatically generated by @propel-code-bot