ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,99 @@
FROM python:3.13-slim-bookworm as base
# Avoid prompts from apt
ENV DEBIAN_FRONTEND=noninteractive
# Configure mirror
ARG ENABLE_MIRROR=false
ENV PIP_INDEX_URL=${ENABLE_MIRROR:+https://mirrors.aliyun.com/pypi/simple}
RUN if [ "$ENABLE_MIRROR" = "true" ]; then \
rm -rfv /etc/apt/sources.list.d/* && \
echo "deb http://ftp.cn.debian.org/debian bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
echo "deb http://ftp.cn.debian.org/debian bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
echo "deb http://ftp.cn.debian.org/debian bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
echo "deb http://ftp.cn.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*; \
fi
# Install dependencies
RUN apt-get update && apt-get install -y \
nodejs \
npm \
git \
xvfb \
x11vnc \
openbox \
supervisor \
novnc \
websockify \
procps \
xdg-utils \
python3-xdg \
x11-xserver-utils \
curl \
--no-install-recommends
# Install noVNC
RUN git clone --depth 1 --branch v1.6.0 https://github.com/novnc/noVNC.git /usr/local/novnc \
&& git clone --depth 1 --branch v0.13.0 https://github.com/novnc/websockify /usr/local/novnc/utils/websockify
# Set up working directory
WORKDIR /app/view_server
# Install Playwright and browsers with dependencies
RUN npm install playwright@1.54
RUN npx playwright install chromium --with-deps
# Set up supervisord configuration
COPY docker/resources/supervisord.conf /etc/supervisor/supervisord.conf
# Copy scripts
COPY docker/resources/start.sh start.sh
COPY docker/resources/playwright-server.js playwright-server.js
COPY docker/resources/x11-setup.sh x11-setup.sh
# Make scripts executable
RUN chmod +x start.sh x11-setup.sh
# Create a simple openbox configuration to only show the browser window
RUN mkdir -p /root/.config/openbox
COPY docker/resources/openbox-rc.xml /root/.config/openbox/rc.xml
ENV PLAYWRIGHT_WS_PATH="default"
ENV PLAYWRIGHT_PORT=37367
ENV NO_VNC_PORT=5901
# Set the display environment variable
ENV DISPLAY=:99
COPY docker/resources/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# MCP Server Container
FROM base
RUN pip install -U --break-system-packages uv
COPY mcp_server_proxy /app/mcp_server_proxy
RUN cd /app/mcp_server_proxy && uv sync --python-preference=only-system
RUN apt-get install -y --no-install-recommends \
wget \
unzip \
libterm-readline-perl-perl \
libmupdf-dev \
vim \
libmagic1
WORKDIR /root/workspace
EXPOSE 4242
HEALTHCHECK --interval=10s --timeout=10s --start-period=10s --retries=12 \
CMD curl -f http://localhost:4242/health || exit 1
CMD ["sh", "-c", "/app/view_server/start.sh && cd /app/mcp_server_proxy && uv run --no-sync -m mcp_server_proxy.main"]
@@ -0,0 +1,7 @@
#!/bin/sh
cd "$(dirname "$0")"
docker build -t mcp-server-base -f Dockerfile_mcp_server . && \
echo "✅ Build image success: mcp-server-base"
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
umask 000
exec "$@"
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<openbox_config xmlns="http://openbox.org/3.4/rc" xmlns:xi="http://www.w3.org/2001/XInclude">
<desktops>
<number>1</number>
</desktops>
<margins>
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
</margins>
<applications>
<application class="*">
<decor>no</decor>
<maximized>yes</maximized>
<fullscreen>yes</fullscreen>
<position>
<x>0</x>
<y>0</y>
</position>
<size>
<width>100%</width>
<height>100%</height>
</size>
<focus>yes</focus>
<desktop>1</desktop>
<layer>normal</layer>
</application>
</applications>
</openbox_config>
@@ -0,0 +1,7 @@
{
"name": "playwright-remote",
"version": "1.0.0",
"dependencies": {
"playwright": "1.51.1"
}
}
@@ -0,0 +1,39 @@
const { chromium } = require("playwright");
// Read ws path from environment variable
const wsPath = process.env.WS_PATH || "default";
const port = process.env.PLAYWRIGHT_PORT || 37367;
(async () => {
console.log("Starting Playwright server...");
// Start the remote debugging server
const browserServer = await chromium.launchServer({
headless: false,
port: port,
wsPath: wsPath,
args: [
"--start-fullscreen",
"--start-maximized",
"--window-size=1280,1280",
"--window-position=0,0",
"--disable-infobars",
"--no-default-browser-check",
"--kiosk",
"--disable-session-crashed-bubble",
"--noerrdialogs",
"--force-device-scale-factor=1.0",
"--disable-features=DefaultViewportMetaTag",
"--force-device-width=1280",
],
});
console.log(`Playwright server running: ${browserServer.wsEndpoint()}`);
// Keep the process running
process.on("SIGINT", async () => {
console.log("Shutting down Playwright server...");
// await browser.close();
await browserServer.close();
process.exit(0);
});
})();
@@ -0,0 +1,8 @@
#!/bin/bash
set -e
echo "Starting services..."
echo "DISPLAY=$DISPLAY"
# Start supervisord to manage all processes
exec supervisord -c /etc/supervisor/supervisord.conf
@@ -0,0 +1,64 @@
[supervisord]
logfile=/var/log/supervisord.log
logfile_maxbytes=50MB
loglevel=info
[include]
files = /etc/supervisor/conf.d/*.conf
[program:xvfb]
command=Xvfb :99 -screen 0 2560x2560x24 -dpi 192 -ac -nolisten tcp
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:openbox]
command=openbox-session
environment=DISPLAY=:99
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:x11setup]
command=/app/view_server/x11-setup.sh
environment=DISPLAY=:99
autorestart=false
startsecs=0
startretries=0
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:x11vnc]
command=x11vnc -display :99 -forever -shared -nopw -geometry 1280x1280 -scale 1:1 -nomodtweak -noxdamage
autorestart=true
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:novnc]
command=/usr/local/novnc/utils/novnc_proxy --vnc localhost:5900 --listen %(ENV_NO_VNC_PORT)s
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:playwright-server]
command=node /app/view_server/playwright-server.js
environment=DISPLAY=:99,WS_PATH=%(ENV_PLAYWRIGHT_WS_PATH)s,PLAYWRIGHT_PORT=%(ENV_PLAYWRIGHT_PORT)s
autorestart=true
priority=30
startsecs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
@@ -0,0 +1,29 @@
#!/bin/bash
# Make sure DISPLAY is set
if [ -z "$DISPLAY" ]; then
export DISPLAY=:99
fi
# Set background to black to make black bars less obvious
xsetroot -solid "#000000"
# Force X11 to use the exact screen dimensions without any offsets
xrandr --output default --mode 1280x1280 --pos 0x0
# Set proper DPI settings for the display
echo "Xft.dpi: 96" | xrdb -merge
echo "Xft.antialias: 1" | xrdb -merge
echo "Xft.hinting: 1" | xrdb -merge
echo "Xft.hintstyle: hintfull" | xrdb -merge
echo "Xft.rgba: rgb" | xrdb -merge
# Disable any screen savers or power management
xset s off
xset -dpms
xset s noblank
# Ensure consistent scaling
xrandr --dpi 96
echo "X11 environment configured for optimal display"
@@ -0,0 +1,25 @@
[project]
name = "mcp-proxy"
version = "0.1.0"
description = "MCP Proxy"
requires-python = ">=3.12"
dependencies = [
"docker",
"aiohttp",
"playwright==1.52",
"pydantic",
"pydantic-settings",
"fastapi[standard]",
"typer",
"aiofiles",
"python-dotenv",
"websockets",
"pyyaml",
"mcp==1.12.4",
"httpx[http2]",
"requests>=2.32.5",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
@@ -0,0 +1,16 @@
import os
from pathlib import Path
mcp_servers_path = os.getenv(
"MCP_SERVERS_PATH",
str((Path(__file__).parent.parent.parent.parent / "mcp_servers").resolve()),
)
mcp_servers_config_path = os.getenv(
"MCP_SERVERS_CONFIG_PATH", str((Path(mcp_servers_path) / "mcp_config.py").resolve())
)
mcp_tool_schema_path = os.getenv(
"MCP_TOOL_SCHEMA_PATH",
str((Path(mcp_servers_path) / "mcp_tool_schema.json").resolve()),
)
@@ -0,0 +1,44 @@
import asyncio
import datetime
import logging
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from .mcp_server_proxy import MCPServerProxy
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
mcp = MCPServerProxy(
name="MCP Server",
stateless_http=False,
host="0.0.0.0",
port=4242,
log_level="DEBUG",
)
@mcp.custom_route("/health", methods=["GET"])
async def health(request: Request) -> Response:
return JSONResponse(
{
"status": "success",
"message": "MCP Server is healthy",
"last_active": datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f"),
}
)
async def main():
logger.info("Starting MCP Server Proxy...")
await mcp.initialize()
await mcp.run_streamable_http_async()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,192 @@
from contextlib import AsyncExitStack
from datetime import timedelta
import json
import os
from pathlib import Path
import traceback
from typing import Any
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.server.fastmcp import Context
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
import logging
from mcp.client.stdio import stdio_client
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from mcp.types import LoggingMessageNotificationParams
from .configs import mcp_servers_path
logger = logging.getLogger(__name__)
class MCPServerExecutor:
def __init__(self, name: str, config: dict):
self._name = name
self._config = config
self._session = None
self._exit_stack = None
self._lock = asyncio.Lock()
self._init_event = asyncio.Event()
self._terminate_event = asyncio.Event()
async def call_tool(
self,
name: str,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
convert_result: bool = False,
) -> Any:
"""Call a tool by name with arguments."""
await self._ensure_server_ready()
async def progress_callback_adapter(
progress: float, total: float | None, message: str | None
):
logger.info(
f"progress_callback: tool={name}, {progress}, {total}, {message}"
)
await self.progress_callback(
progress=progress,
total=total,
message=message,
context=context,
)
result = await self._session.call_tool(
name, arguments, progress_callback=progress_callback_adapter
)
return result.content, result.structuredContent
async def _ensure_server_ready(self):
if not self._session:
asyncio.create_task(self._start_tool_server())
await self._init_event.wait()
async def _start_tool_server(
self,
):
if self._session is not None:
return
async with self._lock:
if self._session is not None:
return
name: str = self._name
config: dict = self._config
try:
logger.info(f"Starting tool server {name} with config {config}")
exit_stack = AsyncExitStack()
await exit_stack.__aenter__()
# Create client context and enter it
if config.get("type") == "sse":
read_stream, write_stream = await exit_stack.enter_async_context(
sse_client(
url=config.get("url", ""),
headers=config.get("headers", {}),
timeout=config.get("timeout", 60 * 10),
sse_read_timeout=config.get("sse_read_timeout", 60 * 10),
auth=config.get("auth", None),
)
)
elif config.get("type") == "streamable_http":
read_stream, write_stream, _ = await exit_stack.enter_async_context(
streamablehttp_client(
url=config.get("url", ""),
headers=config.get("headers", {}),
timeout=config.get("timeout", 60 * 10),
sse_read_timeout=config.get("sse_read_timeout", 60 * 10),
auth=config.get("auth", None),
)
)
else: # stdio
env = config.get("env", {})
env.update(os.environ)
server_params = StdioServerParameters(
command=config.get("command", ""),
args=config.get("args", []),
env=env,
cwd=str(Path(mcp_servers_path) / config.get("cwd", "")),
)
read_stream, write_stream = await exit_stack.enter_async_context(
stdio_client(server=server_params)
)
async def log_callback(params: LoggingMessageNotificationParams):
logger.info(f"MCP Server {name} >>> {params}")
# Create session and tool manager
session = await exit_stack.enter_async_context(
ClientSession(
read_stream,
write_stream,
logging_callback=log_callback,
read_timeout_seconds=timedelta(
seconds=config.get("read_timeout", 60 * 10)
),
)
)
await session.initialize()
self._session = session
self._exit_stack = exit_stack
logger.info(f"Starting tool server success! {name}: {config}")
self._init_event.set()
await self._terminate_event.wait()
except Exception as e:
logger.error(
f"Error starting tool server {name}: {config}\n{traceback.format_exc()}"
)
self._init_event.set()
try:
await self._exit_stack.aclose()
except Exception:
pass
raise e
async def cleanup(self):
self._terminate_event.set()
self._session = None
self._exit_stack = None
async def progress_callback(
self,
progress: float,
total: float | None,
message: str | None,
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
):
def get_session_id() -> str:
try:
return context.request_context.request.headers.get("Mcp-Session-Id")
except Exception as e:
logger.error(f"Error getting session id: {e}")
return None
if "tool_call_card_novnc_window" == message:
"""Show the VNC window"""
vnc_tool_card = {
"type": "tool_call_card_novnc_window",
"card_data": {
"title": "VNC Window",
"url": f"/novnc/{get_session_id()}/vnc.html?autoconnect=true&reconnect=true&quality=9&compression=9&show_dot=0&resize=local",
"token": get_session_id(),
},
}
message = f"""\
\n\n
```tool_card
{json.dumps(vnc_tool_card, indent=2, ensure_ascii=False)}
```
\n\n
"""
if context:
await context.report_progress(
progress=progress, total=total, message=message
)
@@ -0,0 +1,28 @@
import json
from importlib.util import spec_from_file_location, module_from_spec
from pathlib import Path
from .configs import mcp_servers_config_path, mcp_tool_schema_path
class MCPServerLoader:
def __init__(self):
pass
def load_mcp_servers_config(self):
mcp_config = self._load_mcp_config()
return mcp_config.get("mcpServers", {})
def load_mcp_tool_schema(self):
with open(mcp_tool_schema_path, "r") as f:
return json.load(f)
def _load_mcp_config(self):
path = Path(mcp_servers_config_path).resolve()
assert path.exists(), f"MCP servers config file not found: {path}"
spec = spec_from_file_location("mcp_servers_config", path)
module = module_from_spec(spec)
spec.loader.exec_module(module)
mcp_config = getattr(module, "mcp_config")
return mcp_config
@@ -0,0 +1,117 @@
import traceback
from typing import Any, Callable, Sequence
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.resources import Resource
from mcp.server.fastmcp.tools import Tool, ToolManager
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestT
from mcp.types import ContentBlock, ToolAnnotations
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
import logging
from mcp.server.fastmcp.tools.base import Tool as ServerTool
from mcp.types import Tool as ClientTool
from .mcp_server_executor import MCPServerExecutor
from .mcp_server_loader import MCPServerLoader
logger = logging.getLogger(__name__)
class MCPServerProxy(FastMCP):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._mcp_tool_schema: dict[str, list[dict[str, Any]]] = {}
self._mcp_server_executors: dict[str, MCPServerExecutor] = {}
self._mcp_server_loader = MCPServerLoader()
async def initialize(self):
self._load_tool_schema()
self._load_mcp_servers()
async def call_tool(
self, name: str, arguments: dict[str, Any]
) -> Sequence[ContentBlock] | dict[str, Any]:
"""Call a tool by name with arguments."""
try:
context = self.get_context()
request_mcp_server_executor = self._get_request_mcp_server_executor(
tool_name=name
)
return await request_mcp_server_executor.call_tool(
name, arguments, context=context, convert_result=True
)
except:
logger.error(f"Error calling tool {name}: {traceback.format_exc()}")
raise
async def list_tools(self) -> list[ClientTool]:
"""List all available tools."""
try:
request_mcp_servers = self._get_request_mcp_servers()
request_tools = [
tool
for server_name, server_tools in self._mcp_tool_schema.items()
if server_name in request_mcp_servers
for tool in server_tools
]
return [
ClientTool(
name=tool.get("name", ""),
title=tool.get("title", ""),
description=tool.get("description", ""),
inputSchema=tool.get("inputSchema", {}),
outputSchema=tool.get("outputSchema", {}),
annotations=tool.get("annotations", {}),
_meta=tool.get("_meta", {}),
)
for tool in request_tools
]
except:
logger.error(f"Error listing tools: {traceback.format_exc()}")
raise
def _load_tool_schema(self):
self._mcp_tool_schema = self._mcp_server_loader.load_mcp_tool_schema()
mcp_tool_schema = ""
for server_name, server_tools in self._mcp_tool_schema.items():
mcp_tool_schema += f" {server_name}:\n"
for tool in server_tools:
mcp_tool_schema += f" - {tool.get('name', '')}\n"
logger.info(f"Loaded MCP tool schema: mcp_tool_schema={mcp_tool_schema}")
def _load_mcp_servers(self):
for name, config in self._mcp_server_loader.load_mcp_servers_config().items():
self._mcp_server_executors[name] = MCPServerExecutor(name, config)
logger.info(f"Added MCP server executor: {name}")
def _get_request_mcp_servers(self) -> list[str]:
context = self.get_context()
request_servers = context.request_context.request.headers.get("MCP_SERVERS")
if request_servers:
return [server.strip() for server in request_servers.split(",")]
return []
def _get_request_mcp_server_executor(self, tool_name: str) -> MCPServerExecutor:
request_mcp_servers = self._get_request_mcp_servers()
request_tools = {
server_name: tool
for server_name, server_tools in self._mcp_tool_schema.items()
if server_name in request_mcp_servers
for tool in server_tools
if tool.get("name", "") == tool_name
}
if not request_tools:
raise ToolError(f"Tool {tool_name} not found")
else:
if len(request_tools) > 1:
logger.warning(
f"Tool {tool_name} found in multiple MCP servers: {request_tools}"
)
server_name = list(request_tools.keys())[0]
return self._mcp_server_executors[server_name]
@@ -0,0 +1,102 @@
import asyncio
import json
import subprocess
import logging
from pathlib import Path
from typing import Any, AsyncGenerator, List
from mcp import ClientSession
from mcp.types import (
LoggingMessageNotificationParams,
ElicitResult,
ElicitRequestParams,
)
from mcp.client.streamable_http import streamablehttp_client
from mcp.shared.context import RequestContext
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
async def mcp_client(
url: str,
token: str,
session_id: str = None,
mcp_servers: List[str] = [
"readweb-server",
"browser-server",
"browseruse-server",
"documents-csv-server",
"documents-docx-server",
"documents-pptx-server",
"documents-pdf-server",
"documents-txt-server",
"download-server",
"intelligence-code-server",
"intelligence-think-server",
"intelligence-guard-server",
"media-audio-server",
"media-image-server",
"media-video-server",
"parxiv-server",
"terminal-server",
"wayback-server",
"wiki-server",
"googlesearch-server",
],
) -> AsyncGenerator[ClientSession, None]:
headers = {
"Authorization": f"Bearer {token}",
"MCP_SERVERS": ",".join(mcp_servers),
}
if session_id:
headers["SESSION_ID"] = session_id
async with streamablehttp_client(
url=url,
headers=headers,
) as (
read_stream,
write_stream,
get_session_id,
):
async def logging_callback(params: LoggingMessageNotificationParams):
logger.info(f"Receive logging callback: {params}")
async def elicitation_callback(
context: RequestContext["ClientSession", Any],
params: ElicitRequestParams,
) -> ElicitResult:
logger.info(f"Receive elicitation callback: {params}")
return ElicitResult(action="accept", content={"user_name": "John"})
async with ClientSession(
read_stream=read_stream,
write_stream=write_stream,
logging_callback=logging_callback,
elicitation_callback=elicitation_callback,
) as session:
logger.info(f"MCP client connected: url={url}")
await session.initialize()
logger.info(
f"MCP client session initialized: url={url}, session_id={get_session_id()}"
)
yield session
async def progress_callback(progress: float, total: float | None, message: str | None):
logger.info(
f"Receive progress callback: progress={progress}, total={total}, message={message}"
)
if "```tool_card" in message:
data = json.loads(message.split("```tool_card")[1].split("```")[0])
vnc_url = f"{base_url}{data.get('card_data').get('url')}"
logger.info(f"VNC URL: {vnc_url}")
subprocess.run(["open", vnc_url])
@@ -0,0 +1,78 @@
import asyncio
from contextlib import AsyncExitStack
from datetime import timedelta
import json
import logging
from pathlib import Path
from mcp import ClientSession, StdioServerParameters, stdio_client
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
logger = logging.getLogger(__name__)
config = {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "~/workspace"],
}
async def server_session():
async with AsyncExitStack() as exit_stack:
# Create client context and enter it
if config.get("type") == "sse":
read_stream, write_stream = await exit_stack.enter_async_context(
sse_client(
url=config.get("url", ""),
headers=config.get("headers", {}),
timeout=config.get("timeout", 60),
sse_read_timeout=config.get("sse_read_timeout", 60 * 5),
auth=config.get("auth", None),
)
)
elif config.get("type") == "streamable_http":
read_stream, write_stream, _ = await exit_stack.enter_async_context(
streamablehttp_client(
url=config.get("url", ""),
headers=config.get("headers", {}),
timeout=config.get("timeout", 120),
sse_read_timeout=config.get("sse_read_timeout", 60 * 5),
auth=config.get("auth", None),
)
)
else: # stdio
base_folder = Path(__file__).parent
server_params = StdioServerParameters(
command=config.get("command", ""),
args=config.get("args", []),
env=config.get("env", {}),
cwd=str(base_folder / config.get("cwd", "")),
)
read_stream, write_stream = await exit_stack.enter_async_context(
stdio_client(server=server_params)
)
# Create session and tool manager
session = await exit_stack.enter_async_context(
ClientSession(
read_stream,
write_stream,
read_timeout_seconds=timedelta(seconds=config.get("read_timeout", 120)),
)
)
await session.initialize()
yield session
async def test():
async for session in server_session():
ls = await session.list_tools()
assert ls and ls.tools, "list_tools return null"
tools = ls.tools
logger.info(f"list_tools return:\n - {'\n - '.join([t.name for t in tools])}")
print(tools[0])
if __name__ == "__main__":
asyncio.run(test())
@@ -0,0 +1 @@
tool_test_cases = [{"tool_name": "read_url", "args": {"url": "https://www.baidu.com"}}]
@@ -0,0 +1,112 @@
import asyncio
import base64
import hashlib
import hmac
import json
import subprocess
import logging
import os
import time
from pathlib import Path
from typing import Any, AsyncGenerator
from mcp import ClientSession
from mcp.types import (
LoggingMessageNotificationParams,
ElicitResult,
ElicitRequestParams,
)
from mcp.client.streamable_http import streamablehttp_client
from mcp.shared.context import RequestContext
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
LOCAL_MCP_TOKEN_SECRET = "123321"
def _jwt_part(value: dict) -> str:
raw = json.dumps(value, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
def gen_local_mcp_token(app: str = "mcp-gateway-debug") -> str:
secret = os.getenv("MCP_GATEWAY_TOKEN_SECRET", LOCAL_MCP_TOKEN_SECRET)
header = {"alg": "HS256", "typ": "JWT"}
payload = {"app": app, "version": 1, "time": time.time()}
signing_input = f"{_jwt_part(header)}.{_jwt_part(payload)}"
signature = hmac.new(
secret.encode(),
signing_input.encode(),
hashlib.sha256,
).digest()
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b"=").decode()
return f"{signing_input}.{encoded_signature}"
if __name__ == "__main__":
base_url, token = (
"http://localhost:8000",
gen_local_mcp_token(),
)
asyncio.run(McpClient.mcp_test_client(base_url, token))
class McpClient:
async def mcp_test_client(
base_url: str, token: str
) -> AsyncGenerator[ClientSession, None]:
url = f"{base_url}/mcp"
async with streamablehttp_client(
url=url,
headers={
"Authorization": f"Bearer {token}",
"MCP_SERVERS": "readweb-server,browser-server,browseruse-server,documents-csv-server,documents-docx-server,documents-pptx-server,documents-pdf-server,documents-txt-server,download-server,intelligence-code-server,intelligence-think-server,intelligence-guard-server,media-audio-server,media-image-server,media-video-server,parxiv-server,terminal-server,wayback-server,wiki-server,googlesearch-server",
# "SESSION_ID": "CHAT_WLDEV",
},
) as (
read_stream,
write_stream,
get_session_id,
):
async def logging_callback(params: LoggingMessageNotificationParams):
logger.info(f"Receive logging callback: {params}")
async def elicitation_callback(
context: RequestContext["ClientSession", Any],
params: ElicitRequestParams,
) -> ElicitResult:
logger.info(f"Receive elicitation callback: {params}")
return ElicitResult(action="accept", content={"user_name": "John"})
async with ClientSession(
read_stream=read_stream,
write_stream=write_stream,
logging_callback=logging_callback,
elicitation_callback=elicitation_callback,
) as session:
logger.info(f"MCP client connected: url={url}")
await session.initialize()
logger.info(
f"MCP client session initialized: url={url}, session_id={get_session_id()}"
)
yield session
async def progress_callback(progress: float, total: float | None, message: str | None):
logger.info(
f"Receive progress callback: progress={progress}, total={total}, message={message}"
)
if "```tool_card" in message:
data = json.loads(message.split("```tool_card")[1].split("```")[0])
vnc_url = f"{base_url}{data.get('card_data').get('url')}"
logger.info(f"VNC URL: {vnc_url}")
subprocess.run(["open", vnc_url])
@@ -0,0 +1,51 @@
import asyncio
import json
import logging
from pathlib import Path
import sys
import os
from dotenv import load_dotenv
# Add the project root to Python path
sys.path.insert(0, str(Path(__file__).parent.parent))
from core.mcp_client import mcp_client, progress_callback
from core.test_data import tool_test_cases
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
load_dotenv()
read_arg = lambda e: (os.getenv(f"URL_{e}"), os.getenv(f"TOKEN_{e}"))
url, token = read_arg("REMOTE")
# url, token = read_arg("GW_DEBUG")
# url, token = read_arg("MCP_DEBUG")
async def main():
async for session in mcp_client(url, token):
ls = await session.list_tools()
assert ls and ls.tools, "list_tools return null"
tools = ls.tools
logger.info(f"list_tools return:\n - {'\n - '.join([t.name for t in tools])}")
t = tools[0]
assert t.name, "tool.name is null"
assert t.inputSchema, "tool.inputSchema is null"
assert t.outputSchema, "tool.outputSchema is null"
for t in tool_test_cases:
tool_name = t["tool_name"]
args = t["args"]
logger.info(f"call tool: {tool_name}")
result = await session.call_tool(tool_name, args, progress_callback=progress_callback)
logger.info(f"tool result: {result.content[0].text[:300]}")
input("Press Enter to continue...")
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff