Skip to content
Merged
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
62 changes: 62 additions & 0 deletions examples/codex_with_skill_and_mcp/README.md
Original file line number Diff line number Diff line change
@@ -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/<name>/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.
52 changes: 52 additions & 0 deletions examples/codex_with_skill_and_mcp/README.zh.md
Original file line number Diff line number Diff line change
@@ -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/<name>/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)可用。
```
106 changes: 106 additions & 0 deletions examples/codex_with_skill_and_mcp/main.py
Original file line number Diff line number Diff line change
@@ -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())
44 changes: 44 additions & 0 deletions examples/codex_with_skill_and_mcp/mcp_server.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions examples/codex_with_skill_and_mcp/skills/weather-style/SKILL.md
Original file line number Diff line number Diff line change
@@ -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:

`<city>: <condition>, <temperature>. Have a nice day!`

Do not invent conditions — always take them from the tool result.
6 changes: 6 additions & 0 deletions veadk/runtime/codex/tools_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading