Skip to content

Commit 3274acf

Browse files
authored
Merge pull request #1 from AlexAlbala/feature/mcp_support
Feature/mcp support
2 parents f99f505 + 4c2bed5 commit 3274acf

File tree

11 files changed

+147
-4
lines changed

11 files changed

+147
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies = [
3636
"pydantic>=2.0.0",
3737
"openai>=1.63.2",
3838
"pyyaml>=6.0.0",
39+
"fastmcp>=0.4.1",
3940
]
4041

4142
[project.urls]

src/asktheapi_team_builder/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from .team_builder import TeamBuilder
2-
from .agent_builder import AgentBuilder
3-
from .models import Agent, Tool, Message
4-
from .api_spec_handler import (
1+
from .core.team_builder import TeamBuilder
2+
from .core.agent_builder import AgentBuilder
3+
from .core.models import Agent, Tool, Message
4+
from .core.api_spec_handler import (
55
APISpecHandler,
66
APISpecClassification,
77
APISpecClassificationResult,

src/asktheapi_team_builder/core/__init__.py

Whitespace-only changes.

src/asktheapi_team_builder/api_spec_handler.py renamed to src/asktheapi_team_builder/core/api_spec_handler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,19 @@ class APISpecClassification(BaseModel):
1414
class APISpecClassificationResult(BaseModel):
1515
specs: List[APISpecClassification]
1616

17+
class APISpecAgentToolResult(BaseModel):
18+
name: str
19+
description: str
20+
jsonschema: dict
21+
path: str
22+
method: str
23+
1724
class APISpecAgentResult(BaseModel):
1825
name: str
1926
description: str
2027
system_prompt: str
2128
user_prompt: str
29+
tools: List[APISpecAgentToolResult]
2230

2331
class APISpecHandler:
2432
def __init__(self, headers: dict = {}):
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from fastmcp import FastMCP
2+
from pydantic import BaseModel
3+
4+
from asktheapi_team_builder.core.api_spec_handler import APISpecHandler
5+
from asktheapi_team_builder.core.tool_builder import build_tool_function
6+
7+
class MCPConfig(BaseModel):
8+
transport: str = "sse"
9+
port: int = 8000
10+
name: str = "asktheapi_mcp"
11+
12+
class MCPService():
13+
def __init__(self, mcp_config: MCPConfig):
14+
self.mcp = FastMCP(name=mcp_config.name, port=mcp_config.port)
15+
self.mcp_config = mcp_config
16+
self.api_spec_handler = APISpecHandler()
17+
18+
async def _create_from_spec(self, url_spec: str, headers: dict = {}):
19+
spec_content = await self.api_spec_handler.download_url_spec(url_spec)
20+
classification_result = await self.api_spec_handler.classify_spec(spec_content)
21+
for group_spec in classification_result.specs:
22+
agent_result = await self.api_spec_handler.generate_agent_for_group(
23+
group_spec,
24+
spec_content
25+
)
26+
for tool in agent_result.tools:
27+
self.mcp.add_tool(build_tool_function(agent_result, tool, headers))
28+
29+
return self.mcp
30+
31+
async def _run_mcp(self):
32+
self.mcp.run(self.mcp_config.transport)
33+
34+
async def start_from_spec(self, url_spec: str, headers: dict = {}):
35+
await self._create_from_spec(url_spec, headers)
36+
await self._run_mcp()

tests/services/test_mcp_service.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import pytest
2+
from unittest.mock import AsyncMock, MagicMock, patch
3+
from asktheapi_team_builder.services.mcp_service import MCPService, MCPConfig
4+
from asktheapi_team_builder.core.api_spec_handler import APISpecHandler
5+
6+
@pytest.fixture
7+
def mock_api_spec_handler():
8+
handler = AsyncMock(spec=APISpecHandler)
9+
handler.download_url_spec.return_value = "mock_spec_content"
10+
handler.classify_spec.return_value = MagicMock(
11+
specs=[
12+
MagicMock(
13+
group_name="test_group",
14+
endpoints=["/test"]
15+
)
16+
]
17+
)
18+
handler.generate_agent_for_group.return_value = MagicMock(
19+
tools=[
20+
MagicMock(
21+
name="test_tool",
22+
description="A test tool",
23+
method="GET",
24+
path="/test",
25+
jsonschema={}
26+
)
27+
]
28+
)
29+
return handler
30+
31+
@pytest.fixture
32+
def mock_fast_mcp():
33+
with patch("asktheapi_team_builder.services.mcp_service.FastMCP") as mock:
34+
instance = MagicMock()
35+
mock.return_value = instance
36+
yield instance
37+
38+
@pytest.fixture
39+
def mcp_service(mock_fast_mcp, mock_api_spec_handler):
40+
with patch("asktheapi_team_builder.services.mcp_service.APISpecHandler", return_value=mock_api_spec_handler):
41+
config = MCPConfig(transport="sse", port=8000, name="test_mcp")
42+
service = MCPService(config)
43+
return service
44+
45+
@pytest.mark.asyncio
46+
async def test_mcp_service_initialization(mcp_service, mock_fast_mcp):
47+
# Assert
48+
assert mcp_service.mcp_config.name == "test_mcp"
49+
assert mcp_service.mcp_config.port == 8000
50+
assert mcp_service.mcp_config.transport == "sse"
51+
52+
@pytest.mark.asyncio
53+
async def test_create_from_spec(mcp_service, mock_api_spec_handler, mock_fast_mcp):
54+
# Arrange
55+
url_spec = "http://example.com/spec"
56+
headers = {"Authorization": "Bearer token"}
57+
58+
# Act
59+
result = await mcp_service._create_from_spec(url_spec, headers)
60+
61+
# Assert
62+
mock_api_spec_handler.download_url_spec.assert_called_once_with(url_spec)
63+
mock_api_spec_handler.classify_spec.assert_called_once_with("mock_spec_content")
64+
mock_api_spec_handler.generate_agent_for_group.assert_called_once()
65+
assert result == mock_fast_mcp
66+
# Verify that add_tool was called
67+
assert mock_fast_mcp.add_tool.called
68+
69+
@pytest.mark.asyncio
70+
async def test_run_mcp(mcp_service, mock_fast_mcp):
71+
# Act
72+
await mcp_service._run_mcp()
73+
74+
# Assert
75+
mock_fast_mcp.run.assert_called_once_with("sse")
76+
77+
@pytest.mark.asyncio
78+
async def test_start_from_spec(mcp_service, mock_api_spec_handler):
79+
# Arrange
80+
url_spec = "http://example.com/spec"
81+
headers = {"Authorization": "Bearer token"}
82+
83+
# Act
84+
await mcp_service.start_from_spec(url_spec, headers)
85+
86+
# Assert
87+
mock_api_spec_handler.download_url_spec.assert_called_once_with(url_spec)
88+
mcp_service.mcp.run.assert_called_once_with("sse")
89+
90+
@pytest.mark.asyncio
91+
async def test_mcp_config_defaults():
92+
# Act
93+
config = MCPConfig()
94+
95+
# Assert
96+
assert config.transport == "sse"
97+
assert config.port == 8000
98+
assert config.name == "asktheapi_mcp"

0 commit comments

Comments
 (0)