mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""Turn MCP tools into ToolRegistry-ready callables.
|
|
|
|
Each MCP tool becomes a sync callable (so it fits the registry's `execute` contract, which
|
|
the engine already runs via `asyncio.to_thread`). The callable bridges back to the live
|
|
async session on the server loop via `run_coroutine_threadsafe`. We attach `ToolMetadata`
|
|
(category="mcp", `requires_approval` per config) so the PermissionEngine gates it, and an
|
|
explicit OpenAI schema built straight from the MCP `inputSchema` for fidelity.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import re
|
|
from typing import Any, Awaitable, Callable
|
|
|
|
import aisuite as ai
|
|
|
|
from .config import MCPServerDef
|
|
|
|
CallAsync = Callable[[str, dict[str, Any]], Awaitable[Any]]
|
|
|
|
_NAME_OK = re.compile(r"[^a-zA-Z0-9_-]")
|
|
_MAX_NAME = 64 # OpenAI function-name limit
|
|
|
|
|
|
def tool_name(server: str, tool: str) -> str:
|
|
"""`mcp__<server>__<tool>`, sanitized to OpenAI's `[A-Za-z0-9_-]{1,64}` rule."""
|
|
base = f"mcp__{_NAME_OK.sub('_', server)}__{_NAME_OK.sub('_', tool)}"
|
|
if len(base) > _MAX_NAME:
|
|
base = base[:_MAX_NAME]
|
|
return base
|
|
|
|
|
|
def _openai_schema(name: str, mcp_tool: Any) -> dict[str, Any]:
|
|
params = getattr(mcp_tool, "inputSchema", None) or {
|
|
"type": "object",
|
|
"properties": {},
|
|
}
|
|
description = (getattr(mcp_tool, "description", None) or "")[:1024]
|
|
return {
|
|
"type": "function",
|
|
"function": {"name": name, "description": description, "parameters": params},
|
|
}
|
|
|
|
|
|
def _filtered(mcp_tools: list[Any], server: MCPServerDef) -> list[Any]:
|
|
out = mcp_tools
|
|
if server.include_tools is not None:
|
|
allow = set(server.include_tools)
|
|
out = [t for t in out if t.name in allow]
|
|
if server.exclude_tools:
|
|
block = set(server.exclude_tools)
|
|
out = [t for t in out if t.name not in block]
|
|
return out
|
|
|
|
|
|
def build_callables(
|
|
server: MCPServerDef,
|
|
mcp_tools: list[Any],
|
|
call_async: CallAsync,
|
|
loop: asyncio.AbstractEventLoop,
|
|
*,
|
|
timeout: float = 120.0,
|
|
) -> list[Callable[..., Any]]:
|
|
"""Wrap a server's (filtered) MCP tools as registry-ready callables."""
|
|
callables: list[Callable[..., Any]] = []
|
|
for mcp_tool in _filtered(mcp_tools, server):
|
|
name = tool_name(server.name, mcp_tool.name)
|
|
remote = mcp_tool.name
|
|
|
|
def _invoke(_remote: str = remote, **kwargs: Any) -> Any:
|
|
future = asyncio.run_coroutine_threadsafe(call_async(_remote, kwargs), loop)
|
|
return future.result(timeout)
|
|
|
|
# We attach the schema + metadata explicitly (rather than via `ai.tool`, which would
|
|
# try to derive a schema from this `**kwargs` wrapper): the registry reads both attrs.
|
|
_invoke.__name__ = name
|
|
_invoke.__doc__ = (
|
|
getattr(mcp_tool, "description", None)
|
|
or f"MCP tool {remote} from {server.name}"
|
|
)
|
|
_invoke.__aisuite_tool_metadata__ = ai.ToolMetadata(
|
|
name=name,
|
|
category="mcp",
|
|
risk_level="medium",
|
|
capabilities=[server.name],
|
|
requires_approval=server.requires_approval,
|
|
)
|
|
_invoke.__coworker_schema__ = _openai_schema(name, mcp_tool)
|
|
callables.append(_invoke)
|
|
return callables
|