diff --git a/examples/codex_with_skill_and_mcp/README.md b/examples/codex_with_skill_and_mcp/README.md new file mode 100644 index 00000000..cf401c16 --- /dev/null +++ b/examples/codex_with_skill_and_mcp/README.md @@ -0,0 +1,62 @@ +# codex_with_skill_and_mcp + +A `runtime="codex"` agent that uses **both a local skill and an MCP tool** on a +chat backend (Volcengine Ark). + +``` +codex_with_skill_and_mcp/ +├── main.py # the agent + a sample run +├── mcp_server.py # a tiny stdio MCP server (get_weather) +└── skills/ + └── weather-style/ + └── SKILL.md # a local skill (how to phrase weather answers) +``` + +## What it shows + +The agent has two tools wired the ordinary VeADK way: + +- a **skill** — `SkillToolset(skills=[load_skill_from_dir(...)])` +- an **MCP tool** — `MCPToolset(...)` (stdio here; swap for streamable-HTTP) + +Ask *"What's the weather in Beijing?"* and the agent should reply with the +skill's format using the tool's data, e.g.: + +``` +Beijing: sunny, 28°C. Have a nice day! +``` + +## How the codex runtime handles it + +Codex runs the turn instead of ADK's LLM flow, and it only speaks the Responses +API — so the two tools take different paths: + +- **Skill** → materialized into Codex's on-disk skill directory + (`$CODEX_HOME/skills//SKILL.md`) and discovered by Codex's native skill + system. Backend-independent. +- **MCP tool** → Codex can't be handed MCP tools directly (it presents them to + the model as a `namespace` tool the chat backend rejects), so the runtime's + Responses shim advertises them to the backend as plain `function` tools and + executes them itself, invisibly to Codex. + +Both are handled by the runtime — the agent code is just normal tool wiring. + +## Run + +```bash +pip install openai-codex # bundles the Codex CLI binary +# Ark (or another OpenAI-compatible chat) credentials: +export MODEL_AGENT_API_KEY=... +export MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3 +export MODEL_AGENT_NAME=deepseek-v4-flash-260425 + +python examples/codex_with_skill_and_mcp/main.py +``` + +## Notes + +- Tools execute inside the runtime's shim, so they are invisible to Codex and do + not surface as separate ADK events (tracing/UI) today. +- Interactive MCP auth (mid-turn credential prompts) is not driven under + `runtime="codex"`; static auth (headers / bearer token / ve-identity workload + tokens) works. diff --git a/examples/codex_with_skill_and_mcp/README.zh.md b/examples/codex_with_skill_and_mcp/README.zh.md new file mode 100644 index 00000000..713b92c7 --- /dev/null +++ b/examples/codex_with_skill_and_mcp/README.zh.md @@ -0,0 +1,52 @@ +# codex_with_skill_and_mcp + +一个 `runtime="codex"` 的 Agent,在 chat 后端(火山方舟)上**同时使用本地 skill 和 MCP 工具**。 + +``` +codex_with_skill_and_mcp/ +├── main.py # Agent 定义 + 一次示例运行 +├── mcp_server.py # 一个极简的 stdio MCP server(get_weather) +└── skills/ + └── weather-style/ + └── SKILL.md # 一个本地 skill(规定天气回答的措辞) +``` + +## 演示了什么 + +Agent 用最普通的 VeADK 方式挂了两个工具: + +- 一个 **skill**——`SkillToolset(skills=[load_skill_from_dir(...)])` +- 一个 **MCP 工具**——`MCPToolset(...)`(这里用 stdio,可换成 streamable-HTTP) + +问 *“北京天气怎么样?”*,Agent 会用 skill 规定的格式、结合工具返回的数据回答,例如: + +``` +Beijing: sunny, 28°C. Have a nice day! +``` + +## codex runtime 怎么处理 + +Codex 接管了整轮(而不是 ADK 的 LLM flow),且只会说 Responses API——所以两个工具走不同的路: + +- **Skill** → 被物化到 Codex 的磁盘 skill 目录(`$CODEX_HOME/skills//SKILL.md`),由 Codex 原生 skill 机制发现。与后端无关。 +- **MCP 工具** → 不能直接交给 Codex(它会把 MCP 工具以 `namespace` 类型呈现给模型,而 chat 后端不认),所以由 runtime 的 Responses shim 把它们当普通 `function` 工具喂给后端、并**自己执行**,对 Codex 不可见。 + +这些都由 runtime 处理——Agent 代码就是普通的工具挂载。 + +## 运行 + +```bash +pip install openai-codex # 自带 Codex CLI 二进制 +# 方舟(或其他 OpenAI 兼容 chat)凭证: +export MODEL_AGENT_API_KEY=... +export MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3 +export MODEL_AGENT_NAME=deepseek-v4-flash-260425 + +python examples/codex_with_skill_and_mcp/main.py +``` + +## 说明 + +- 工具在 runtime 的 shim 内执行,因此对 Codex 不可见,目前不会作为独立的 ADK 事件出现(trace/前端看不到这步)。 +- 交互式 MCP 鉴权(对话中途弹凭证)在 `runtime="codex"` 下不驱动;静态鉴权(header / bearer token / ve-identity workload token)可用。 +``` diff --git a/examples/codex_with_skill_and_mcp/main.py b/examples/codex_with_skill_and_mcp/main.py new file mode 100644 index 00000000..d91a444e --- /dev/null +++ b/examples/codex_with_skill_and_mcp/main.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A `runtime="codex"` agent that uses both a local skill and an MCP tool. + +On a Codex runtime backed by a chat model (e.g. Volcengine Ark): + +- **Skills** are materialized into Codex's on-disk skill directory and driven by + Codex's native skill system. +- **MCP / function tools** can't be handed to Codex directly (Codex presents + them to the model as a `namespace` tool the chat backend rejects), so the + runtime's shim advertises them to the backend as plain functions and executes + them itself. + +Both are just normal VeADK/ADK wiring — the runtime handles the rest. + +Run: + python examples/codex_with_skill_and_mcp/main.py + +Requires: +- ``pip install openai-codex`` (bundles the Codex CLI binary). +- Ark (or another OpenAI-compatible chat) credentials via ``MODEL_AGENT_API_KEY`` + / ``MODEL_AGENT_API_BASE`` / ``MODEL_AGENT_NAME`` (see the repo .env.example). +""" + +import asyncio +import os +import sys +from pathlib import Path + +from google.adk.skills import load_skill_from_dir +from google.adk.tools.mcp_tool.mcp_session_manager import StdioServerParameters +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from google.adk.tools.skill_toolset import SkillToolset +from google.genai import types + +from veadk import Agent, Runner +from veadk.memory.short_term_memory import ShortTermMemory + +_HERE = Path(__file__).resolve().parent +_SKILL_DIR = _HERE / "skills" / "weather-style" +_MCP_SERVER = _HERE / "mcp_server.py" + + +def build_agent() -> Agent: + # Skill: a local SKILL.md directory, loaded the ADK-native way. The codex + # runtime materializes it into Codex's skill directory. + skill_toolset = SkillToolset(skills=[load_skill_from_dir(str(_SKILL_DIR))]) + + # MCP: a stdio MCP server launched as a subprocess. The codex runtime lists + # its tools and executes them via the shim. Swap StdioServerParameters for + # StreamableHTTPConnectionParams(url=...) to point at a remote MCP server. + weather_mcp = MCPToolset( + connection_params=StdioServerParameters( + command=sys.executable, args=[str(_MCP_SERVER)] + ) + ) + + return Agent( + name="codex_skill_mcp_agent", + description="A codex-runtime agent with a skill and an MCP tool.", + instruction="Help the user. Use your skills and tools when relevant.", + runtime="codex", + model_name=os.getenv("MODEL_AGENT_NAME", "deepseek-v4-flash-260425"), + model_api_base=os.getenv( + "MODEL_AGENT_API_BASE", "https://ark.cn-beijing.volces.com/api/v3" + ), + model_api_key=os.getenv("MODEL_AGENT_API_KEY"), + tools=[skill_toolset, weather_mcp], + ) + + +async def main() -> None: + agent = build_agent() + runner = Runner(agent=agent, short_term_memory=ShortTermMemory()) + await runner.short_term_memory.create_session( + app_name=runner.app_name, user_id=runner.user_id, session_id="s1" + ) + + question = "What's the weather in Beijing?" + print(f"User: {question}\n") + async for event in runner.run_async( + user_id=runner.user_id, + session_id="s1", + new_message=types.Content(role="user", parts=[types.Part(text=question)]), + ): + if not event.content or not event.content.parts: + continue + for part in event.content.parts: + if part.text and not part.thought: + print(f"Agent: {part.text}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/codex_with_skill_and_mcp/mcp_server.py b/examples/codex_with_skill_and_mcp/mcp_server.py new file mode 100644 index 00000000..6a8f9da6 --- /dev/null +++ b/examples/codex_with_skill_and_mcp/mcp_server.py @@ -0,0 +1,44 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A tiny demo MCP server exposing a single ``get_weather`` tool over stdio. + +``main.py`` launches this as a subprocess via an MCP stdio transport, so the +example is self-contained (no separate server to start). Swap it for any real +MCP server — stdio or streamable-HTTP — by changing the connection params in +``main.py``. +""" + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("weather-demo") + +# Canned data keeps the example deterministic and offline. +_DEFAULT = ("cloudy", "22°C") +_TABLE = { + "beijing": ("sunny", "28°C"), + "shanghai": ("rainy", "19°C"), + "shenzhen": ("humid", "31°C"), +} + + +@mcp.tool() +def get_weather(city: str) -> str: + """Return the current weather for a city as 'condition, temperature'.""" + condition, temp = _TABLE.get(city.strip().lower(), _DEFAULT) + return f"{condition}, {temp}" + + +if __name__ == "__main__": + mcp.run() # stdio transport diff --git a/examples/codex_with_skill_and_mcp/skills/weather-style/SKILL.md b/examples/codex_with_skill_and_mcp/skills/weather-style/SKILL.md new file mode 100644 index 00000000..8d5dd8eb --- /dev/null +++ b/examples/codex_with_skill_and_mcp/skills/weather-style/SKILL.md @@ -0,0 +1,13 @@ +--- +name: weather-style +description: How to answer weather questions. Use whenever the user asks about the weather in a city. +--- + +When the user asks about the weather in a city: + +1. Call the `get_weather` tool with that city to get the current conditions. +2. Reply in exactly this format, on one line: + + `: , . Have a nice day!` + +Do not invent conditions — always take them from the tool result. diff --git a/veadk/runtime/codex/tools_bridge.py b/veadk/runtime/codex/tools_bridge.py index 4cbdeaf6..a59ae398 100644 --- a/veadk/runtime/codex/tools_bridge.py +++ b/veadk/runtime/codex/tools_bridge.py @@ -85,6 +85,12 @@ def _add(tool: "BaseTool") -> None: executors[name] = _make_executor(tool, ctx, ToolContext) for entry in getattr(agent, "tools", None) or []: + # Skill toolsets are handled by the skill materialization path + # (veadk.runtime.codex.skills → $CODEX_HOME/skills), driven by Codex's + # own skill system. Skip them here so their internal tools + # (list_skills / load_skill / ...) aren't also exposed via the shim. + if type(entry).__name__ in ("SkillToolset", "SkillsToolset"): + continue if isinstance(entry, BaseToolset): try: tools = await entry.get_tools()