Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ agent = Agent(
Expose the programmatic tools as an MCP server for any MCP-compatible client:

```python
from agent_codemode import codemode_server, configure_server
from agent_codemode import configure_server, run_server
from agent_codemode import ToolRegistry, MCPServerConfig, CodeModeConfig

# Create and configure registry with MCP servers to compose
Expand All @@ -331,7 +331,7 @@ config = CodeModeConfig(
)

configure_server(config=config, registry=registry)
codemode_server.run()
run_server() # stdio; run_server(transport="streamable-http", host=..., port=...) for HTTP
```

Or start with command line:
Expand Down
2 changes: 2 additions & 0 deletions agent_codemode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from .proxy.meta_tools import MetaToolProvider
from .server import configure as configure_server
from .server import mcp as codemode_server
from .server import run as run_server
from .toolset import PYDANTIC_AI_AVAILABLE, CodemodeToolset
from .types import (
CodeModeConfig,
Expand Down Expand Up @@ -90,6 +91,7 @@
"configure_server",
"parallel",
"retry",
"run_server",
"run_with_timeout",
"setup_skills_directory",
# Helpers (from agent_skills)
Expand Down
2 changes: 1 addition & 1 deletion agent_codemode/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""Agent Codemode."""

__version__ = "1.0.1"
__version__ = "1.1.0"
5 changes: 4 additions & 1 deletion agent_codemode/composition/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,10 @@ async def call_tool(tool_name: str, arguments: dict[str, Any]) -> Any:

is_error = False
if isinstance(result, dict):
is_error = result.get("isError", False)
is_error = result.get("isError", result.get("is_error", False))
elif hasattr(result, "is_error"):
# A ``CallToolResult`` of mcp 2, whose fields are snake_case.
is_error = result.is_error
elif hasattr(result, "isError"):
is_error = result.isError

Expand Down
5 changes: 4 additions & 1 deletion agent_codemode/discovery/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ async def call_tool(tool_name: str, arguments: dict[str, Any]) -> Any:
# Check for error response
is_error = False
if isinstance(result, dict):
is_error = result.get("isError", False)
is_error = result.get("isError", result.get("is_error", False))
elif hasattr(result, "is_error"):
# A ``CallToolResult`` of mcp 2, whose fields are snake_case.
is_error = result.is_error
elif hasattr(result, "isError"):
is_error = result.isError

Expand Down
9 changes: 6 additions & 3 deletions agent_codemode/proxy/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ async def _get_http_session(self):
return self._http_session

from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.streamable_http import streamable_http_client

http_ctx = streamablehttp_client(self.url)
# mcp 2: the transport yields the two streams only; the session id
# callback of mcp 1 is gone, and headers or timeouts would be set on
# an ``httpx2.AsyncClient`` passed as ``http_client``.
http_ctx = streamable_http_client(self.url)
self._http_ctx = http_ctx
read_stream, write_stream, _get_session_id = await http_ctx.__aenter__()
read_stream, write_stream = await http_ctx.__aenter__()
http_session = ClientSession(read_stream, write_stream)
self._http_session = http_session
await http_session.__aenter__()
Expand Down
91 changes: 51 additions & 40 deletions agent_codemode/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@

import anyio
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext

from .composition.executor import CodeModeExecutor
from .discovery.registry import ToolRegistry
from .types import CodeModeConfig

logger = logging.getLogger(__name__)

# Create the MCP server
mcp = Server("codemode")
# The MCP server is built at the end of this module, once its handlers
# exist: mcp 2's lowlevel ``Server`` takes them as constructor arguments.

# Global instances (configured at startup)
_registry: Optional[ToolRegistry] = None
Expand Down Expand Up @@ -119,7 +119,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name=name,
description=description,
inputSchema=parameters,
input_schema=parameters,
)
)

Expand All @@ -129,7 +129,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="save_skill",
description="Save a reusable skill (code-based tool composition).",
inputSchema={
input_schema={
"type": "object",
"required": ["name", "code", "description"],
"properties": {
Expand All @@ -144,7 +144,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="run_skill",
description="Execute a saved skill.",
inputSchema={
input_schema={
"type": "object",
"required": ["name"],
"properties": {
Expand All @@ -156,7 +156,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="list_skills",
description="List available skills.",
inputSchema={
input_schema={
"type": "object",
"properties": {
"tags": {"type": "array", "items": {"type": "string"}},
Expand All @@ -166,7 +166,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="delete_skill",
description="Delete a saved skill.",
inputSchema={
input_schema={
"type": "object",
"required": ["name"],
"properties": {
Expand All @@ -177,7 +177,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="get_execution_history",
description="Get recent tool execution history.",
inputSchema={
input_schema={
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 10},
Expand All @@ -187,7 +187,7 @@ def _build_tools() -> list[types.Tool]:
types.Tool(
name="add_mcp_server",
description="Add a new MCP server to discover tools from.",
inputSchema={
input_schema={
"type": "object",
"required": ["name"],
"properties": {
Expand Down Expand Up @@ -588,25 +588,47 @@ async def handle_add_mcp_server(arguments: dict[str, Any]) -> dict[str, Any]:
# =============================================================================


@mcp.list_tools()
async def list_tools() -> list[types.Tool]:
async def list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""Return the list of available tools."""
config = _config or CodeModeConfig()
if config.allow_direct_tool_calls:
return TOOLS
return [tool for tool in TOOLS if tool.name != "call_tool"]
return types.ListToolsResult(tools=TOOLS)
return types.ListToolsResult(tools=[tool for tool in TOOLS if tool.name != "call_tool"])


async def call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult:
"""Handle tool calls.

@mcp.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
"""Handle tool calls."""
handler = TOOL_HANDLERS.get(name)
A failure is answered as a result with ``is_error`` set, the way mcp 1's
decorator did it: mcp 2's lowlevel server turns an exception raised here
into a JSON-RPC error instead, which an agent cannot read as a tool
outcome.
"""
handler = TOOL_HANDLERS.get(params.name)
if handler is None:
raise ValueError(f"Unknown tool: {name}")
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")],
is_error=True,
)

result = await handler(arguments)
try:
result = await handler(params.arguments or {})
except Exception as e: # every failure is reported to the agent
logger.debug("Tool %s failed", params.name, exc_info=e)
return types.CallToolResult(
content=[types.TextContent(type="text", text=str(e))],
is_error=True,
)
json_str = json.dumps(result, indent=2)
return [types.TextContent(type="text", text=json_str)]
return types.CallToolResult(content=[types.TextContent(type="text", text=json_str)])
Comment on lines +623 to +627


# Create the MCP server
mcp = Server("codemode", on_list_tools=list_tools, on_call_tool=call_tool)


# =============================================================================
Expand All @@ -629,26 +651,15 @@ def run(transport: str = "stdio", host: str = "127.0.0.1", port: int = 8000) ->

if transport == "streamable-http":
import uvicorn
from mcp.server.streamable_http import StreamableHTTPServerTransport
from starlette.applications import Starlette
from starlette.routing import Route

async def handle_mcp(request):
transport_ctx: Any = StreamableHTTPServerTransport(
"/mcp", request.scope, request.receive, request._send
)
async with transport_ctx as transport:
await mcp.run(
transport.read_stream,
transport.write_stream,
mcp.create_initialization_options(),
)

starlette_app = Starlette(
debug=True,
routes=[
Route("/mcp", endpoint=handle_mcp, methods=["POST"]),
],
# Stateless, as before: each request is served on a transport of its
# own. ``host`` is passed on because the SDK enables DNS-rebinding
# protection, with a localhost-only allowlist, when it is a loopback
# address — a server bound elsewhere must not inherit that list.
starlette_app = mcp.streamable_http_app(
streamable_http_path="/mcp",
stateless_http=True,
host=host,
)

uvicorn.run(starlette_app, host=host, port=port)
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,11 @@ See [Integrations](./integrations/index.mdx) for complete documentation.

**Exposed as an MCP Server:**
```python
from agent_codemode import codemode_server, configure_server
from agent_codemode import configure_server, run_server

# Configure with your registry
configure_server(config=config, registry=registry)
codemode_server.run()
run_server()
```

Or run directly:
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/integrations/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ python -m agent_codemode.server
Or programmatically:

```python
from agent_codemode import codemode_server, configure_server, ToolRegistry, MCPServerConfig
from agent_codemode import configure_server, run_server, ToolRegistry, MCPServerConfig

registry = ToolRegistry()
registry.add_server(MCPServerConfig(name="filesystem", command="npx", args=["@anthropic-ai/mcp-server-filesystem"]))

configure_server(registry=registry)
codemode_server.run()
run_server()
```

The server exposes tools for:
Expand Down
5 changes: 2 additions & 3 deletions docs/docs/skills/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -353,16 +353,15 @@ When running Agent Codemode as an MCP server, skills are exposed through dedicat
- **`run_skill`**: Execute a saved skill by name

```python
from agent_codemode import codemode_server, configure_server
from agent_codemode import CodeModeConfig
from agent_codemode import CodeModeConfig, configure_server, run_server

config = CodeModeConfig(
skills_path="./skills",
# ... other config
)

configure_server(config=config)
codemode_server.run()
run_server()
```

## Pydantic AI Integration
Expand Down
2 changes: 1 addition & 1 deletion docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ module.exports = {
},
{
label: 'Bluesky',
href: 'https://assets.datalayer.tech/logos-social-grey/youtube.svg',
href: 'https://assets.datalayer.tech/logos-social-grey/bluesky.svg',
},
{
label: 'LinkedIn',
Expand Down
5 changes: 3 additions & 2 deletions examples/patterns/codemode_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ async def _server():
# Configure with MCP servers
configure_server()

# Run the MCP server (uses FastMCP under the hood)
codemode_server.run()
# Serve it over stdio (the MCP SDK's lowlevel Server under the hood)
from agent_codemode.server import run
run()
Comment on lines +161 to +163

Or from the command line:

Expand Down
4 changes: 2 additions & 2 deletions examples/simple/example_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from pathlib import Path
from typing import Optional

from mcp.server.fastmcp import FastMCP
from mcp.server import MCPServer
from typing_extensions import TypedDict

mcp = FastMCP("example-mcp-server")
mcp = MCPServer("example-mcp-server")

# Base directory for file operations - defaults to /tmp if CWD is not writable
_BASE_DIR: Path | None = None
Expand Down
4 changes: 2 additions & 2 deletions examples/skills/example_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from pathlib import Path
from typing import Optional

from mcp.server.fastmcp import FastMCP
from mcp.server import MCPServer
from typing_extensions import TypedDict

mcp = FastMCP("example-mcp-server")
mcp = MCPServer("example-mcp-server")

# Base directory for file operations - defaults to /tmp if CWD is not writable
_BASE_DIR: Path | None = None
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ classifiers = [
dependencies = [
"agent-skills",
"code-sandboxes",
"mcp[cli]>=1.10.1,<2",
# The MCP Python SDK 2: FastMCP became MCPServer, the lowlevel Server takes
# its handlers as constructor arguments, and clients come with httpx2. See
# https://py.sdk.modelcontextprotocol.io/v2/migration/
"mcp[cli]>=2,<3",
"pydantic>=2.0",
"httpx>=0.24",
]
Expand Down
22 changes: 0 additions & 22 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,6 @@
import pytest


def _rebuild_fastmcp_settings() -> None:
"""Resolve the forward reference in FastMCP's ``Settings`` model.

``mcp.server.fastmcp.server.Settings.lifespan`` is annotated with
``FastMCP``, which is defined further down the same module, and upstream
never calls ``model_rebuild()``. Recent pydantic-settings releases warn
(``IncompleteFieldDefinitionWarning``) when such a model is instantiated,
and this suite turns warnings into errors, so collection fails as soon as
anything constructs a FastMCP server. Rebuilding the model once resolves
the reference for real instead of muting the warning.
"""
try:
from mcp.server.fastmcp.server import Settings
except ImportError: # pragma: no cover - mcp layout changed
return
if not getattr(Settings, "__pydantic_complete__", True):
Settings.model_rebuild()


_rebuild_fastmcp_settings()


@pytest.fixture
def skills_dir(tmp_path: Path) -> Path:
"""Create a temporary skills directory."""
Expand Down