Skip to content

Commit

Permalink
Merge pull request #21 from SecretiveShell/add-sse-server
Browse files Browse the repository at this point in the history
Add sse support
  • Loading branch information
SecretiveShell authored Dec 31, 2024
2 parents a9711e9 + 3bfa1a0 commit f02dbbd
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 28 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ working features:

- MCP tools

- SSE Bridge for external clients

planned features:

- streaming completions are not implemented yet
Expand Down Expand Up @@ -99,6 +101,9 @@ Once the application is running, you can interact with it using the OpenAI API.

View the documentation at [http://yourserver:8000/docs](http://localhost:8000/docs). There is an endpoint to list all the MCP tools available on the server, which you can use to test the application configuration.

## SSE Bridge
MCP-Bridge also provides an SSE bridge for external clients. This lets external chat apps with explicit MCP support use MCP-Bridge as a MCP server. Point your client at the SSE endpoint (http://yourserver:8000/mcp-server/sse) and you should be able to see all the MCP tools available on the server.

## Configuration

To add new MCP servers, edit the config.json file.
Expand Down
5 changes: 4 additions & 1 deletion mcp_bridge/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@
from endpoints import router as endpointRouter
from mcpManagement import router as mcpRouter
from health import router as healthRouter
from mcp_server import router as mcp_server_router
from lifespan import lifespan
from openapi_tags import tags_metadata
from openapi_tags import tags_metadata, version

app = FastAPI(
title="MCP Bridge",
description="A middleware application to add MCP support to openai compatible apis",
version=version,
lifespan=lifespan,
openapi_tags=tags_metadata,
)

app.include_router(endpointRouter)
app.include_router(mcpRouter)
app.include_router(healthRouter)
app.include_router(mcp_server_router)

if __name__ == "__main__":
import uvicorn
Expand Down
6 changes: 4 additions & 2 deletions mcp_bridge/mcpManagement/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

router = APIRouter(prefix="/prompts")


@router.get("")
async def get_prompts() -> dict[str, ListPromptsResult]:
"""Get all prompts from all MCP clients"""
Expand All @@ -16,16 +17,17 @@ async def get_prompts() -> dict[str, ListPromptsResult]:

return prompts


@router.post("/{prompt_name}")
async def get_prompt(prompt_name: str, args: dict[str, str] = {}) -> GetPromptResult:
"""Evaluate a prompt"""

client = await ClientManager.get_client_from_prompt(prompt_name)
if not client:
raise HTTPException(status_code=404, detail=f"Prompt '{prompt_name}' not found")

result = await client.get_prompt(prompt_name, arguments=args)
if not result:
raise HTTPException(status_code=404, detail=f"Prompt '{prompt_name}' not found")

return result
1 change: 1 addition & 0 deletions mcp_bridge/mcpManagement/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

router = APIRouter(prefix="/resources")


@router.get("")
async def get_resources() -> dict[str, ListResourcesResult]:
"""Get all resources from all MCP clients"""
Expand Down
5 changes: 1 addition & 4 deletions mcp_bridge/mcpManagement/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
from .resources import router as resources_router
from .server import router as server_router

router = APIRouter(
prefix="/mcp",
tags=[Tag.mcp_management]
)
router = APIRouter(prefix="/mcp", tags=[Tag.mcp_management])

router.include_router(tools_router)
router.include_router(prompts_router)
Expand Down
12 changes: 8 additions & 4 deletions mcp_bridge/mcpManagement/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,46 @@

router = APIRouter(prefix="/servers")


@router.get("/{server_name}/prompts")
async def get_server_prompts(server_name: str) -> ListPromptsResult:
"""Get all prompts from a specific MCP server"""

client = ClientManager.get_client(server_name)
if not client:
raise HTTPException(status_code=404, detail=f"Server '{server_name}' not found")

return await client.list_prompts()


@router.get("/{server_name}/tools")
async def get_server_tools(server_name: str) -> ListToolsResult:
"""Get all tools from a specific MCP server"""

client = ClientManager.get_client(server_name)
if not client:
raise HTTPException(status_code=404, detail=f"Server '{server_name}' not found")

return await client.list_tools()


@router.get("/{server_name}/resources")
async def get_server_resources(server_name: str) -> ListResourcesResult:
"""Get all resources from a specific MCP server"""

client = ClientManager.get_client(server_name)
if not client:
raise HTTPException(status_code=404, detail=f"Server '{server_name}' not found")

return await client.list_resources()


@router.get("/{server_name}/status")
async def get_server_status(server_name: str) -> McpServerStatus:
"""Get the status of a specific MCP server"""

client = ClientManager.get_client(server_name)
if not client:
raise HTTPException(status_code=404, detail=f"Server '{server_name}' not found")

return await client.status()
4 changes: 3 additions & 1 deletion mcp_bridge/mcpManagement/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

router = APIRouter(prefix="/tools")


@router.get("")
async def get_tools() -> dict[str, ListToolsResult]:
"""Get all tools from all MCP clients"""
Expand All @@ -15,12 +16,13 @@ async def get_tools() -> dict[str, ListToolsResult]:

return tools


@router.post("/{tool_name}/call")
async def call_tool(tool_name: str, arguments: dict[str, str] = {}) -> CallToolResult:
"""Call a tool"""

client = await ClientManager.get_client_from_tool(tool_name)
if not client:
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")

return await client.call_tool(tool_name, arguments)
49 changes: 36 additions & 13 deletions mcp_bridge/mcp_clients/AbstractClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
from typing import Any, Optional
from fastapi import HTTPException
from mcp import ClientSession, McpError
from mcp.types import CallToolResult, ListToolsResult, TextContent, ListResourcesResult, ListPromptsResult, GetPromptResult
from mcp.types import (
CallToolResult,
ListToolsResult,
TextContent,
ListResourcesResult,
ListPromptsResult,
GetPromptResult,
TextResourceContents,
BlobResourceContents,
)
from loguru import logger
from pydantic import AnyUrl
from models.mcpServerStatus import McpServerStatus


Expand All @@ -29,7 +39,7 @@ async def _session_maintainer(self):
except Exception as e:
logger.trace(f"failed to maintain session for {self.name}: {e}")
await asyncio.sleep(0.5)

async def start(self):
asyncio.create_task(self._session_maintainer())

Expand Down Expand Up @@ -60,8 +70,10 @@ async def call_tool(
content=[TextContent(type="text", text=f"Error calling {name}: {e}")],
isError=True,
)

async def get_prompt(self, prompt: str, arguments: dict[str, str]) -> GetPromptResult | None:

async def get_prompt(
self, prompt: str, arguments: dict[str, str]
) -> GetPromptResult | None:
await self._wait_for_session()

try:
Expand All @@ -71,6 +83,17 @@ async def get_prompt(self, prompt: str, arguments: dict[str, str]) -> GetPromptR

return None

async def read_resource(
self, uri: AnyUrl
) -> list[TextResourceContents | BlobResourceContents]:
await self._wait_for_session()
try:
resource = await self.session.read_resource(uri)
return resource.contents
except Exception as e:
logger.error(f"error reading resource: {e}")
return []

async def list_tools(self) -> ListToolsResult:
# if session is None, then the client is not running
# wait to see if it restarts
Expand All @@ -84,15 +107,15 @@ async def list_tools(self) -> ListToolsResult:

async def list_resources(self) -> ListResourcesResult:
await self._wait_for_session()
try:
try:
return await self.session.list_resources()
except Exception as e:
logger.error(f"error listing resources: {e}")
return ListResourcesResult(resources=[])

async def list_prompts(self) -> ListPromptsResult:
await self._wait_for_session()
try:
try:
return await self.session.list_prompts()
except Exception as e:
logger.error(f"error listing prompts: {e}")
Expand All @@ -106,14 +129,14 @@ async def _wait_for_session(self, timeout: int = 10, http_error: bool = True):

except asyncio.TimeoutError:
if http_error:
raise HTTPException(status_code=500, detail="Could not connect to MCP server.")

raise HTTPException(
status_code=500, detail="Could not connect to MCP server."
)

raise TimeoutError("Session initialization timed out.")

async def status(self) -> McpServerStatus:
"""Get the status of the MCP server"""
return McpServerStatus(
name=self.name,
online=self.session is not None,
enabled=True
name=self.name, online=self.session is not None, enabled=True
)
2 changes: 1 addition & 1 deletion mcp_bridge/mcp_clients/McpClientManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def get_client_from_tool(self, tool: str):
for client_tool in list_tools.tools:
if client_tool.name == tool:
return client

async def get_client_from_prompt(self, prompt: str):
for name, client in self.get_clients():
try:
Expand Down
8 changes: 8 additions & 0 deletions mcp_bridge/mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter
from .sse import router as sse_router
from openapi_tags import Tag

__all__ = ["router"]

router = APIRouter(prefix="/mcp-server", tags=[Tag.mcp_server])
router.include_router(sse_router)
Loading

0 comments on commit f02dbbd

Please sign in to comment.