Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6c81d71
Changes to allow custom mcp servers
AnuradhaKaruppiah Oct 22, 2025
8a5a433
Tests for validating custom MCP workers
AnuradhaKaruppiah Oct 23, 2025
8b81146
Add helper functions
AnuradhaKaruppiah Oct 23, 2025
858b114
Merge remote-tracking branch 'upstream/develop' into ak-nat-mcp-custo…
AnuradhaKaruppiah Oct 24, 2025
2d839a2
Update fastapi version
AnuradhaKaruppiah Oct 27, 2025
a84a8e0
Merge remote-tracking branch 'upstream/develop' into ak-nat-mcp-custo…
AnuradhaKaruppiah Oct 27, 2025
2a5be28
Add brief documentation for custom MCP server worker
AnuradhaKaruppiah Oct 27, 2025
41d0ca1
Address review comments
AnuradhaKaruppiah Oct 27, 2025
77fe5cb
Simply custom frontend usage
AnuradhaKaruppiah Oct 27, 2025
20085a1
Update docs with simplified usage
AnuradhaKaruppiah Oct 27, 2025
c5f4c42
Update docs to make auth ownership clear
AnuradhaKaruppiah Oct 28, 2025
f9f635d
Fix CI issues
AnuradhaKaruppiah Oct 28, 2025
931301e
Update dependency
AnuradhaKaruppiah Oct 28, 2025
88fce53
Remove redundant import
AnuradhaKaruppiah Oct 28, 2025
dcb944b
Update docs
AnuradhaKaruppiah Oct 28, 2025
b044976
Renamed to mcp-server.md to better align
AnuradhaKaruppiah Oct 28, 2025
77579ac
Merge remote-tracking branch 'upstream/develop' into ak-nat-mcp-custo…
AnuradhaKaruppiah Oct 28, 2025
1ecf232
Provide a base path for mounting
AnuradhaKaruppiah Oct 28, 2025
2e852ab
Update docs with info about the new base path config
AnuradhaKaruppiah Oct 28, 2025
64b68f9
Display full url
AnuradhaKaruppiah Oct 28, 2025
775ede8
Merge remote-tracking branch 'upstream/develop' into ak-nat-mcp-custo…
AnuradhaKaruppiah Oct 28, 2025
f99fb35
Merge remote-tracking branch 'upstream/develop' into ak-nat-mcp-custo…
AnuradhaKaruppiah Oct 28, 2025
2a84a10
Revert uv.lock to 24b287f4
AnuradhaKaruppiah Oct 28, 2025
3872e7a
Add a validator for base path
AnuradhaKaruppiah Oct 28, 2025
46f5af2
Fix missing type checking
AnuradhaKaruppiah Oct 28, 2025
9ae0ab8
Fix style problems
AnuradhaKaruppiah Oct 28, 2025
de4f1ac
Add a not about the format of the base_path
AnuradhaKaruppiah Oct 28, 2025
2a0a4c5
Cleanup doc strings to fix build
AnuradhaKaruppiah Oct 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/vale/styles/config/vocabularies/nat/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ LLM(s?)
# https://github.com/logpai/loghub/
Loghub
Mem0
[Mm]iddleware
Milvus
[Mm]ixin(s?)
MLflow
Expand Down
253 changes: 253 additions & 0 deletions docs/source/extend/mcp-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Adding a Custom MCP Server Worker

:::{note}
We recommend reading the [MCP Server Guide](../workflows/mcp/mcp-server.md) before proceeding with this documentation, to understand how MCP servers work in NVIDIA NeMo Agent toolkit.
:::

The NVIDIA NeMo Agent toolkit provides a default MCP server worker that publishes your workflow functions as MCP tools. However, you may need to customize the server behavior for enterprise requirements such as authentication, custom endpoints, or telemetry. This guide shows you how to create custom MCP server workers that extend the default implementation.

## When to Create a Custom Worker

Create a custom MCP worker when you need to:
- **Add authentication/authorization**: OAuth, API keys, JWT tokens, or custom auth flows
- **Integrate custom transport protocols**: WebSocket, gRPC, or other communication methods
- **Add logging and telemetry**: Custom logging, metrics collection, or distributed tracing
- **Modify server behavior**: Custom middleware, error handling, or protocol extensions
- **Integrate with enterprise systems**: SSO, audit logging, or compliance requirements

## Creating and Registering a Custom MCP Worker

To extend the NeMo Agent toolkit with custom MCP workers, you need to create a worker class that inherits from {py:class}`~nat.front_ends.mcp.mcp_front_end_plugin_worker.MCPFrontEndPluginWorker` and override the methods you want to customize.

This section provides a step-by-step guide to create and register a custom MCP worker with the NeMo Agent toolkit. A custom status endpoint worker is used as an example to demonstrate the process.

## Step 1: Implement the Worker Class

Create a new Python file for your worker implementation. The following example shows a minimal worker that adds a custom status endpoint to the MCP server.

Each worker is instantiated once when `nat mcp serve` runs. The `create_mcp_server()` method executes during initialization, and `add_routes()` runs after the workflow is built.

<!-- path-check-skip-next-line -->
`src/my_package/custom_worker.py`:
```python
import logging

from mcp.server.fastmcp import FastMCP

from nat.builder.workflow_builder import WorkflowBuilder
from nat.front_ends.mcp.mcp_front_end_plugin_worker import MCPFrontEndPluginWorker

logger = logging.getLogger(__name__)


class CustomStatusWorker(MCPFrontEndPluginWorker):
"""MCP worker that adds a custom status endpoint."""

async def add_routes(self, mcp: FastMCP, builder: WorkflowBuilder):
"""Register tools and add custom server behavior.

This method calls the parent implementation to get all default behavior,
then adds custom routes.

Args:
mcp: The FastMCP server instance
builder: The workflow builder containing functions to expose
"""
# Get all default routes and tool registration
await super().add_routes(mcp, builder)

# Add a custom status endpoint
@mcp.custom_route("/custom/status", methods=["GET"])
async def custom_status(_request):
"""Custom status endpoint with additional server information."""
from starlette.responses import JSONResponse

logger.info("Custom status endpoint called")
return JSONResponse({
"status": "ok",
"server": mcp.name,
"custom_worker": "CustomStatusWorker"
})
```

**Key components**:
- **Inheritance**: Extend {py:class}`~nat.front_ends.mcp.mcp_front_end_plugin_worker.MCPFrontEndPluginWorker`
- **`super().add_routes()`**: Calls parent to get standard tool registration and default routes
- **`@mcp.custom_route()`**: Adds custom HTTP endpoints to the server
- **Clean inheritance**: Use standard Python `super()` pattern to extend behavior

## Step 2: Use the Worker in Your Workflow

Configure your workflow to use the custom worker by specifying the fully qualified class name in the `runner_class` field.

<!-- path-check-skip-next-line -->
`custom_mcp_server_workflow.yml`:
```yaml
general:
front_end:
_type: mcp
runner_class: "my_package.custom_worker.CustomStatusWorker"
name: "my_custom_server"
host: "localhost"
port: 9000


llms:
nim_llm:
_type: nim
model_name: meta/llama-3.3-70b-instruct

functions:
search:
_type: tavily_internet_search

workflow:
_type: react_agent
llm_name: nim_llm
tool_names: [search]
```

## Step 3: Run and Test Your Server

Start your server using the NeMo Agent toolkit CLI:

```bash
nat mcp serve --config_file custom_mcp_server_workflow.yml
```

**Expected output**:
```
INFO: Started server process [12345]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://localhost:9000 (Press CTRL+C to quit)
```

**Test the server** with the MCP client:

```bash
# List available tools
nat mcp client tool list --url http://localhost:9000/mcp

# Call a tool
nat mcp client tool call search \
--url http://localhost:9000/mcp \
--json-args '{"question": "When is the next GTC event?"}'

# Test the custom status endpoint
curl http://localhost:9000/custom/status
```

**Expected response from custom endpoint**:
```json
{
"status": "ok",
"server": "my_custom_server",
"custom_worker": "CustomStatusWorker"
}
```

## Understanding Inheritance and Extension

### Using `super().add_routes()`

When extending {py:class}`~nat.front_ends.mcp.mcp_front_end_plugin_worker.MCPFrontEndPluginWorker`, call `super().add_routes()` to get all default functionality:

- **Health endpoint**: `/health` for server status checks
- **Workflow building**: Processes your workflow configuration
- **Function-to-tool conversion**: Registers NeMo Agent toolkit functions as MCP tools
- **Debug endpoints**: Additional routes for development

Most workers call `super().add_routes()` first to ensure all standard NeMo Agent toolkit tools are registered, then add custom features:

```python
async def add_routes(self, mcp: FastMCP, builder: WorkflowBuilder):
# Get all default behavior from parent
await super().add_routes(mcp, builder)

# Add your custom features
@mcp.custom_route("/my/endpoint", methods=["GET"])
async def my_endpoint(_request):
return JSONResponse({"custom": "data"})
```

### Overriding `create_mcp_server()`

Override `create_mcp_server()` when you need to use a different MCP server implementation:

```python
async def create_mcp_server(self) -> FastMCP:
from my_custom_mcp import CustomFastMCP

return CustomFastMCP(
name=self.front_end_config.name,
host=self.front_end_config.host,
port=self.front_end_config.port,
# Custom parameters
auth_provider=self.get_auth_provider(),
)
```

**Authentication ownership**: When you override `create_mcp_server()`, your worker controls authentication. If you need custom auth (JWT, OAuth2, API keys), configure it inside `create_mcp_server()`. Any front-end config auth settings are optional hints and may be ignored by your worker.

### Accessing Configuration

Your worker has access to configuration through instance variables:

- **`self.front_end_config`**: MCP server configuration
- `name`: Server name
- `host`: Server host address
- `port`: Server port number
- `debug`: Debug mode flag

- **`self.full_config`**: Complete NeMo Agent toolkit configuration
- `general`: General settings including front end config
- `llms`: LLM configurations
- `functions`: Function configurations
- `workflow`: Workflow configuration

**Example using configuration**:

```python
async def create_mcp_server(self) -> FastMCP:
# Access server name from config
server_name = self.front_end_config.name

# Customize based on debug mode
if self.front_end_config.debug:
logger.info(f"Creating debug server: {server_name}")

return FastMCP(
name=server_name,
host=self.front_end_config.host,
port=self.front_end_config.port,
debug=self.front_end_config.debug,
)
```

## Summary

This guide provides a step-by-step process to create custom MCP server workers in the NeMo Agent toolkit. The custom status worker demonstrates how to:

1. Extend {py:class}`~nat.front_ends.mcp.mcp_front_end_plugin_worker.MCPFrontEndPluginWorker`
2. Override `add_routes()` and use `super()` to get default behavior
3. Override `create_mcp_server()` to use a different server implementation. When doing so, implement your own authentication and authorization logic within that server.

Custom workers enable enterprise features like authentication, telemetry, and integration with existing infrastructure without modifying NeMo Agent toolkit core code.
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Adding an Authentication Provider <./extend/adding-an-authentication-provider.md
Integrating AWS Bedrock Models <./extend/integrating-aws-bedrock-models.md>
Cursor Rules Developer Guide <./extend/cursor-rules-developer-guide.md>
Adding a Telemetry Exporter <./extend/telemetry-exporters.md>
Adding a Custom MCP Server Worker <./extend/mcp-server.md>
```

```{toctree}
Expand Down
17 changes: 17 additions & 0 deletions docs/source/workflows/mcp/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ nat mcp serve --config_file examples/getting_started/simple_calculator/configs/c
--tool_names calculator
```

### Mounting at Custom Paths
By default, the MCP server is available at the root path (such as `http://localhost:9901/mcp`). You can mount the server at a custom base path by setting `base_path` in your configuration file:

```yaml
general:
front_end:
_type: mcp
name: "my_server"
base_path: "/api/v1"
```

With this configuration, the MCP server will be accessible at `http://localhost:9901/api/v1/mcp`. This is useful when deploying MCP servers that need to be mounted at specific paths for reverse proxy configurations or service mesh architectures.

:::{note}
The `base_path` feature requires the `streamable-http` transport. SSE transport does not support custom base paths.
:::

## Displaying MCP Tools published by an MCP server

To list the tools published by the MCP server you can use the `nat mcp client tool list` command. This command acts as an MCP client and connects to the MCP server running on the specified URL (defaults to `http://localhost:9901/mcp` for streamable-http, with backwards compatibility for `http://localhost:9901/sse`).
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependencies = [
"colorama~=0.4.6",
"datasets~=4.0", # workaround for uv's solver choosing different versions of datasets based on sys_platform
"expandvars~=1.0",
"fastapi~=0.115.5",
"fastapi~=0.120.1",
"httpx~=0.27",
"jinja2~=3.1",
"jsonpath-ng~=1.7",
Expand Down
4 changes: 4 additions & 0 deletions src/nat/front_ends/mcp/mcp_front_end_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class MCPFrontEndConfig(FrontEndBaseConfig, name="mcp"):
description="Transport type for the MCP server (default: streamable-http, backwards compatible with sse)")
runner_class: str | None = Field(
default=None, description="Custom worker class for handling MCP routes (default: built-in worker)")
base_path: str | None = Field(default=None,
description="Base path to mount the MCP server at (e.g., '/api/v1'). "
"If specified, the server will be accessible at http://host:port{base_path}/mcp. "
"If None, server runs at root path /mcp.")

server_auth: OAuth2ResourceServerConfig | None = Field(
default=None, description=("OAuth 2.0 Resource Server configuration for token verification."))
Expand Down
Loading
Loading