Skip to content

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

Merged
merged 9 commits into from
May 16, 2025

Conversation

kaposke
Copy link
Contributor

@kaposke kaposke commented May 8, 2025

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 and StreamableHttpServerTransport. 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:

{
  "mcpServers": {
    "nango-google-drive": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://localhost:3003/mcp",
        "--header",
        "Authorization:<your-secret-key>",
        "--header",
        "provider-config-key:google-drive",
        "--header",
        "connection-id:<your-connection-id>"
      ]
    }
  }
}

Don't add spaces to the headers


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 to MCP 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/responses

Potential 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

Copy link

linear bot commented May 8, 2025

NAN-3102 MCP prototype

Copy link

cubic-dev-ai bot commented May 8, 2025

Your mrge subscription is currently inactive. Please reactivate your subscription to receive AI reviews and use mrge.

@kaposke kaposke marked this pull request as ready for review May 14, 2025 20:41
.setTag('nango.providerConfigKey', providerConfig.unique_key);

const logCtx = await logContextGetter.create(
{ operation: { type: 'action', action: 'run' }, expiresAt: defaultOperationExpiration.action() },
Copy link
Contributor Author

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?

Copy link
Collaborator

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

@kaposke kaposke requested a review from a team May 14, 2025 20:51
Copy link
Collaborator

@TBonnin TBonnin left a 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);
Copy link
Collaborator

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?

Copy link
Contributor Author

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,
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

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'
Copy link
Collaborator

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?

Copy link
Contributor Author

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
Copy link
Collaborator

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?

Copy link
Contributor Author

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) {
Copy link
Collaborator

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);
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Collaborator

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)
Copy link
Collaborator

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() },
Copy link
Collaborator

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' }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Member

@khaliqgant khaliqgant left a 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

Copy link
Collaborator

@TBonnin TBonnin left a 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

@kaposke kaposke merged commit 5dcf955 into master May 16, 2025
17 checks passed
@kaposke kaposke deleted the gui/NAN-3102/nango-mcp-server branch May 16, 2025 17:11
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.

3 participants