Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
750 changes: 402 additions & 348 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ includes:

mcp-proxy:
taskfile: mcp-proxy/Taskfile.yaml
flatten: true
dir: ./mcp-proxy

vars:
CARGO_COMPONENTS: "cargo-sort cargo-audit cargo-llvm-cov cargo-machete cargo-deny typos-cli"
Expand Down
18 changes: 10 additions & 8 deletions mcp-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ name = "slim-mcp-proxy"
path = "src/main.rs"

[dependencies]
agntcy-slim = "1.0.0"
agntcy-slim = "1.0.2"
# from the slim repo
# agntcy-slim = { git = "https://github.com/agntcy/slim.git", branch = "main" }
agntcy-slim-auth = "0.5.0"
agntcy-slim-config = "0.6.0"
agntcy-slim-datapath = "0.11.1"
agntcy-slim-service = "0.8.4"
agntcy-slim-session = "0.1.4"
agntcy-slim-signal = "0.1.4"
agntcy-slim-config = "0.6.2"
agntcy-slim-datapath = "0.11.3"
agntcy-slim-service = "0.8.6"
agntcy-slim-session = "0.1.6"
agntcy-slim-signal = "0.1.4"
async-trait = "0.1"
clap = "4.5.37"
futures-util = "0.3.31"
rand = "0.9.1"
rmcp = { version = "0.1.5", features = ["client", "transport-sse"] }
rmcp = { version = "0.14.0", features = [
"client",
"transport-streamable-http-client-reqwest",
] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1.41"
4 changes: 2 additions & 2 deletions mcp-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ You can use the commands provided in the Taskfile to run the client and server l
### Run the SLIM node
To run the SLIM node, use the following command:
```bash
task mcp-proxy:run-slim
task mcp-proxy:test:run-slim
```
### Run the SLIM-MCP proxy
To run the SLIM-MCP proxy, use the following command:
```bash
task mcp-proxy:run-mcp-proxy
task mcp-proxy:run
```
### Run the MCP Server
To run the MCP server example, use the following command:
Expand Down
24 changes: 15 additions & 9 deletions mcp-proxy/Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

---

version: '3'
version: "3"

silent: true
set: [pipefail]
Expand All @@ -13,37 +12,44 @@ vars:
CARGO_COMPONENTS: "cargo-sort cargo-audit cargo-llvm-cov cargo-machete@0.7.0 cargo-deny@0.17.0 typos-cli"

tasks:
test:run-mcp-server:
mcp-proxy:test:run-slim:
desc: run slim instance
cmds:
- |
cd examples/config/
docker run -it -v ./slim-server-config.yaml:/config.yaml -p 46357:46357 ghcr.io/agntcy/slim:1.0.2 /slim --config /config.yaml

mcp-proxy:test:run-mcp-server:
desc: run mcp server using SSE transport
cmds:
- |
cd examples/mcp-server/
uv run main.py --port 8000

run-mcp-proxy:
mcp-proxy:run:
desc: run mcp proxy
dir: '{{.TASKFILE_DIR}}'
dir: "{{.TASKFILE_DIR}}"
cmds:
- |
cargo run -- --config config/mcp-proxy-config.yaml \
--svc-name slim/0 --name org/mcp/proxy \
--mcp-server http://localhost:8000/sse
--mcp-server http://localhost:8000/mcp

test:run-mcp-client:
mcp-proxy:test:run-mcp-client:
desc: run a simple MCP client that use SLIM as transport protocol
cmds:
- |
cd examples/mcp-slim-client
uv run main.py

test:reinstall-and-run-mcp-client:
mcp-proxy:test:reinstall-and-run-mcp-client:
desc: run a simple MCP client that use SLIM as transport protocol
cmds:
- |
cd examples/mcp-slim-client
uv run --reinstall main.py

build:strip:
mcp-proxy:build:strip:
desc: "Build the project and strip the debug symbols"
cmds:
- task: :build
Expand Down
21 changes: 21 additions & 0 deletions mcp-proxy/examples/config/slim-server-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright AGNTCY Contributors (https://github.com/agntcy)
# SPDX-License-Identifier: Apache-2.0

tracing:
log_level: info
display_thread_names: true
display_thread_ids: true

runtime:
n_cores: 0
thread_name: "slim-data-plane"
drain_timeout: 10s

services:
slim/0:
dataplane:
servers:
- endpoint: "0.0.0.0:46357"
tls:
insecure: true
clients: []
6 changes: 3 additions & 3 deletions mcp-proxy/examples/mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# MCP Client
This is a simple example of an MCP client that uses the SLIM protocol to transport messages to the server.
# MCP Server
This is a simple example of an MCP server that uses the SSE transport.

## How to run the code
You can run the code using the command provided in the Taskfile:
```bash
task mcp-proxy:run-mcp-client
task mcp-proxy:test:run-mcp-server
```
95 changes: 66 additions & 29 deletions mcp-proxy/examples/mcp-server/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Copyright AGNTCY Contributors (https://github.com/agntcy)
# SPDX-License-Identifier: Apache-2.0

import anyio
import click
import httpx
import mcp.types as types
from mcp.server.lowlevel import Server
from pydantic import FileUrl
import uvicorn
import contextlib
import logging
from collections.abc import AsyncIterator

from mcp.server.sse import SseServerTransport
from pydantic import FileUrl
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Mount, Route
import uvicorn
from starlette.middleware.cors import CORSMiddleware
from starlette.types import Receive, Scope, Send
from starlette.routing import Mount

SAMPLE_RESOURCES = {
"greeting": "Hello! This is a sample text resource.",
Expand Down Expand Up @@ -64,10 +66,22 @@ def create_messages(

return messages

@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")

def main(port: int):
# Configure logging
logger = logging.getLogger(__name__)

@click.command()
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--log-level",
default="debug",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
def main(port: int, log_level: str,) -> int:
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server("mcp-server")

# Tools
Expand Down Expand Up @@ -121,19 +135,20 @@ async def read_resource(uri: FileUrl) -> str | bytes:
raise ValueError(f"Unknown resource: {uri}")

# send a log notification for the resource read
await app.request_context.session.send_log_message("info", "read_resource", f"client read resource {uri}")
await app.request_context.session.send_log_message(level="info", data=f"client read resource {uri}", logger="read_resource_stream" , related_request_id=app.request_context.request_id)

return SAMPLE_RESOURCES[name]

@app.subscribe_resource()
async def subscribe_resources(uri: FileUrl):
# send a log notification for the subscription
await app.request_context.session.send_log_message("info", "subscribe_resource", f"client sent a subscription for resource {uri}")
await app.request_context.session.send_log_message(level="info", data="subscribe_resource", logger="subscribe_resource_stream" , related_request_id=app.request_context.request_id)


@app.unsubscribe_resource()
async def unsubscribe_resources(uri: FileUrl):
# send a log notification for the usubscription
await app.request_context.session.send_log_message("info", "unsubscribe_resource", f"client sent an unsubscription resource {uri}")
await app.request_context.session.send_log_message(level="info", data="unsubscribe_resource", logger="unsubscribe_resource_stream" , related_request_id=app.request_context.request_id)

# Prompt
@app.list_prompts()
Expand Down Expand Up @@ -175,27 +190,49 @@ async def get_prompt(
description="A simple prompt with optional context and topic arguments",
)

# setup server
sse = SseServerTransport("/messages/")

async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await app.run(
streams[0], streams[1], app.create_initialization_options()
)
return Response()
# Create the session manager with our app and event store
session_manager = StreamableHTTPSessionManager(
app=app,
event_store=None, # Enable resumability
json_response=False,
stateless=True,
)

# ASGI handler for streamable HTTP connections
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
await session_manager.handle_request(scope, receive, send)

@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
"""Context manager for managing session manager lifecycle."""
async with session_manager.run():
logger.info("Application started with StreamableHTTP session manager!")
try:
yield
finally:
logger.info("Application shutting down...")

# Create an ASGI application using the transport
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
Mount("/mcp", app=handle_streamable_http),
],
lifespan=lifespan,
)

# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header
# for browser-based clients (ensures 500 errors get proper CORS headers)
starlette_app = CORSMiddleware(
starlette_app,
allow_origins=["*"], # Allow all origins - adjust as needed for production
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)

uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="127.0.0.1", port=port)

return 0

if __name__ == "__main__":
main()
6 changes: 1 addition & 5 deletions mcp-proxy/examples/mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@ version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"click>=8.1.8",
"httpx>=0.28.1",
"mcp==1.6.0",
]
dependencies = ["click>=8.2.0", "httpx>=0.28.1", "mcp==1.26.0"]
Loading