Skip to content

Commit 9de6705

Browse files
committed
feat(vefaas): add mcp server pre-commit
1 parent f962f17 commit 9de6705

File tree

9 files changed

+227
-22
lines changed

9 files changed

+227
-22
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies = [
2121
"opentelemetry-exporter-otlp>=1.35.0",
2222
"opentelemetry-instrumentation-logging>=0.56b0",
2323
"wrapt>=1.17.2", # For patching built-in functions
24+
"fastmcp>=2.11.3",
2425
]
2526

2627
[project.scripts]

tests/test_tracing.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@
3333
from opentelemetry.sdk import trace as trace_sdk
3434
from opentelemetry.sdk.trace.export import BatchSpanProcessor
3535
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
36-
OTLPSpanExporter,
37-
)
36+
OTLPSpanExporter,
37+
)
3838

3939
APP_NAME = "app"
4040
USER_ID = "testuser"
4141
SESSION_ID = "testsession"
4242

43+
4344
def init_exporters():
4445
cozeloop_exporter = CozeloopExporter(
4546
config=CozeloopExporterConfig(
@@ -68,13 +69,15 @@ def init_exporters():
6869
)
6970
return [cozeloop_exporter, apmplus_exporter, tls_exporter]
7071

72+
7173
def gen_span_processor(endpoint: str):
7274
otlp_exporter = OTLPSpanExporter(
7375
endpoint=endpoint,
7476
)
7577
span_processor = BatchSpanProcessor(otlp_exporter)
7678
return span_processor
7779

80+
7881
@pytest.mark.asyncio
7982
async def test_tracing():
8083
exporters = init_exporters()
@@ -86,6 +89,7 @@ async def test_tracing():
8689
# TODO: Ensure the tracing provider is set correctly after loading SDK
8790
# TODO: Ensure the tracing provider is set correctly after loading SDK
8891

92+
8993
@pytest.mark.asyncio
9094
async def test_tracing_with_global_provider():
9195
exporters = init_exporters()
@@ -113,5 +117,3 @@ async def test_tracing_with_apmplus_global_provider():
113117

114118
# apmplus exporter won't init again
115119
assert len(tracer.exporters) == 4 # with extra 2 built-in exporters
116-
117-

veadk/cli/main.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ def init():
9898
)
9999

100100
deploy_mode_options = {
101-
"1": "A2A Server",
101+
"1": "A2A/MCP Server",
102102
"2": "VeADK Studio",
103103
"3": "VeADK Web / Google ADK Web",
104104
}
105105

106106
deploy_mode = Prompt.ask(
107107
"""Choose your deploy mode:
108-
1. A2A Server
108+
1. A2A/MCP Server
109109
2. VeADK Studio
110110
3. VeADK Web / Google ADK Web
111111
""",
@@ -118,13 +118,36 @@ def init():
118118
print("Invalid deploy mode, set default to A2A Server")
119119
deploy_mode = deploy_mode_options["1"]
120120

121+
# Sub-choice for A2A/MCP Server
122+
server_type = None
123+
if deploy_mode == deploy_mode_options["1"]: # A2A/MCP Server
124+
server_type_options = {
125+
"1": "A2A Server",
126+
"2": "MCP Server",
127+
}
128+
129+
server_type_choice = Prompt.ask(
130+
"""Choose server type:
131+
1. A2A Server
132+
2. MCP Server
133+
""",
134+
default="1",
135+
)
136+
137+
if server_type_choice in server_type_options:
138+
server_type = server_type_options[server_type_choice]
139+
else:
140+
print("Invalid server type, set default to A2A Server")
141+
server_type = server_type_options["1"]
142+
121143
setting_values = {
122144
"VEFAAS_APPLICATION_NAME": vefaas_application_name,
123145
"GATEWAY_NAME": gateway_name,
124146
"GATEWAY_SERVICE_NAME": gateway_service_name,
125147
"GATEWAY_UPSTREAM_NAME": gateway_upstream_name,
126148
"USE_STUDIO": deploy_mode == deploy_mode_options["2"],
127149
"USE_ADK_WEB": deploy_mode == deploy_mode_options["3"],
150+
"USE_MCP": server_type == "MCP Server" if server_type else False,
128151
}
129152

130153
shutil.copytree(template_dir, target_dir)

veadk/cli/services/vefaas/template/deploy.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pathlib import Path
1717

1818
from veadk.cloud.cloud_agent_engine import CloudAgentEngine
19+
from fastmcp.client import Client
1920

2021
SESSION_ID = "cloud_app_test_session"
2122
USER_ID = "cloud_app_test_user"
@@ -26,11 +27,11 @@
2627
GATEWAY_UPSTREAMNAME = ""
2728
USE_STUDIO = False
2829
USE_ADK_WEB = False
30+
USE_MCP = False
2931

3032

3133
async def main():
3234
engine = CloudAgentEngine()
33-
3435
cloud_app = engine.deploy(
3536
path=str(Path(__file__).parent / "src"),
3637
application_name=VEFAAS_APPLICATION_NAME,
@@ -39,17 +40,43 @@ async def main():
3940
gateway_upstream_name=GATEWAY_UPSTREAMNAME,
4041
use_studio=USE_STUDIO,
4142
use_adk_web=USE_ADK_WEB,
43+
use_mcp=USE_MCP,
4244
)
4345

44-
if not USE_STUDIO and not USE_ADK_WEB:
45-
response_message = await cloud_app.message_send(
46-
"How is the weather like in Beijing?", SESSION_ID, USER_ID
47-
)
48-
print(f"VeFaaS application ID: {cloud_app.vefaas_application_id}")
49-
print(f"Message ID: {response_message.messageId}")
50-
print(
51-
f"Response from {cloud_app.vefaas_endpoint}: {response_message.parts[0].root.text}"
52-
)
46+
if not USE_STUDIO and (not USE_ADK_WEB):
47+
if not USE_MCP:
48+
response_message = await cloud_app.message_send(
49+
"How is the weather like in Beijing?", SESSION_ID, USER_ID
50+
)
51+
print(f"VeFaaS application ID: {cloud_app.vefaas_application_id}")
52+
print(f"Message ID: {response_message.messageId}")
53+
print(
54+
f"Response from {cloud_app.vefaas_endpoint}: {response_message.parts[0].root.text}"
55+
)
56+
else:
57+
# cloud_app = CloudApp(vefaas_application_name=VEFAAS_APPLICATION_NAME)
58+
endpoint = cloud_app._get_vefaas_endpoint()
59+
print(f"endpoint:{endpoint}")
60+
# Connect to MCP server
61+
client = Client(f"{endpoint}/mcp")
62+
63+
async with client:
64+
# List available tools
65+
tools = await client.list_tools()
66+
print(f"tool_0: {tools[0].__dict__}\n")
67+
68+
# Call run_agent tool, pass user input and session information
69+
res = await client.call_tool(
70+
"run_agent",
71+
{
72+
"user_input": "How is the weather like in Beijing?",
73+
"session_id": SESSION_ID,
74+
"user_id": USER_ID,
75+
},
76+
)
77+
print(f"VeFaaS application ID: {cloud_app.vefaas_application_id}")
78+
print(f"Response from {cloud_app.vefaas_endpoint}: {res}")
79+
5380
else:
5481
print(f"Web is running at: {cloud_app.vefaas_endpoint}")
5582

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import argparse
17+
from agent import agent, app_name, short_term_memory
18+
from veadk.tracing.base_tracer import BaseTracer
19+
from veadk.tracing.telemetry.opentelemetry_tracer import OpentelemetryTracer
20+
from veadk import Agent
21+
from veadk.memory.short_term_memory import ShortTermMemory
22+
from veadk.runner import Runner
23+
from fastmcp import FastMCP
24+
25+
26+
# ==============================================================================
27+
# Tracer Config ================================================================
28+
29+
TRACERS: list[BaseTracer] = []
30+
31+
exporters = []
32+
if os.getenv("VEADK_TRACER_APMPLUS", "").lower() == "true":
33+
from veadk.tracing.telemetry.exporters.apmplus_exporter import APMPlusExporter
34+
35+
exporters.append(APMPlusExporter())
36+
37+
if os.getenv("VEADK_TRACER_COZELOOP", "").lower() == "true":
38+
from veadk.tracing.telemetry.exporters.cozeloop_exporter import CozeloopExporter
39+
40+
exporters.append(CozeloopExporter())
41+
42+
if os.getenv("VEADK_TRACER_TLS", "").lower() == "true":
43+
from veadk.tracing.telemetry.exporters.tls_exporter import TLSExporter
44+
45+
exporters.append(TLSExporter())
46+
47+
TRACERS.append(OpentelemetryTracer(exporters=exporters))
48+
49+
50+
agent.tracers.extend(TRACERS)
51+
if not getattr(agent, "before_model_callback", None):
52+
agent.before_model_callback = []
53+
if not getattr(agent, "after_model_callback", None):
54+
agent.after_model_callback = []
55+
if not getattr(agent, "after_tool_callback", None):
56+
agent.after_tool_callback = []
57+
for tracer in TRACERS:
58+
if tracer.tracer_hook_before_model not in agent.before_model_callback:
59+
agent.before_model_callback.append(tracer.tracer_hook_before_model)
60+
if tracer.tracer_hook_after_model not in agent.after_model_callback:
61+
agent.after_model_callback.append(tracer.tracer_hook_after_model)
62+
if tracer.tracer_hook_after_tool not in agent.after_tool_callback:
63+
agent.after_tool_callback.append(tracer.tracer_hook_after_tool)
64+
65+
# Tracer Config ================================================================
66+
# ==============================================================================
67+
68+
69+
class VeMCPServer:
70+
def __init__(self, agent: Agent, app_name: str, short_term_memory: ShortTermMemory):
71+
self.agent = agent
72+
self.app_name = app_name
73+
self.short_term_memory = short_term_memory
74+
75+
self.runner = Runner(
76+
agent=self.agent,
77+
short_term_memory=self.short_term_memory,
78+
app_name=app_name,
79+
user_id="", # waiting for tool call to provide user_id
80+
)
81+
82+
def build(self) -> FastMCP:
83+
# Create MCP server
84+
mcp = FastMCP(name=self.app_name)
85+
86+
@mcp.tool
87+
async def run_agent(
88+
user_input: str,
89+
user_id: str = "unknown_user",
90+
session_id: str = "unknown_session",
91+
) -> str:
92+
"""
93+
Execute agent with user input and return final output
94+
Args:
95+
user_input: str, user_id: str = "unknown_user", session_id: str = "unknown_session"
96+
Returns:
97+
final_output: str
98+
"""
99+
# Set user_id for runner
100+
self.runner.user_id = user_id
101+
102+
# Running agent and get final output
103+
final_output = await self.runner.run(
104+
messages=user_input,
105+
session_id=session_id,
106+
)
107+
108+
return final_output
109+
110+
return mcp
111+
112+
113+
if __name__ == "__main__":
114+
parser = argparse.ArgumentParser(description="MCP Server")
115+
parser.add_argument(
116+
"--transport", default="http", help="Transport type (default: http)"
117+
)
118+
parser.add_argument(
119+
"--host", default="0.0.0.0", help="Host address (default: 0.0.0.0)"
120+
)
121+
parser.add_argument(
122+
"--port", type=int, default=8000, help="Port number (default: 8000)"
123+
)
124+
parser.add_argument("--log-level", default="INFO", help="Log level (default: INFO)")
125+
126+
args = parser.parse_args()
127+
128+
server = VeMCPServer(
129+
agent=agent,
130+
app_name=app_name,
131+
short_term_memory=short_term_memory,
132+
)
133+
mcp = server.build()
134+
mcp.run(transport=args.transport, host=args.host, port=args.port)

veadk/cli/services/vefaas/template/src/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ opensearch-py==2.8.0
33
agent-pilot-sdk>=0.0.9 # extra dep for prompt optimization in veadk studio
44
typer>=0.16.0
55
uvicorn[standard]
6-
fastapi
6+
fastapi
7+
fastmcp

veadk/cli/services/vefaas/template/src/run.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ python3 -m pip install uvicorn[standard]
3838

3939
python3 -m pip install fastapi
4040

41+
python3 -m pip install fastmcp
42+
4143
USE_STUDIO=${USE_STUDIO:-False}
4244
USE_ADK_WEB=${USE_ADK_WEB:-False}
45+
USE_MCP=${USE_MCP:-False}
4346

4447
if [ "$USE_STUDIO" = "True" ]; then
4548
echo "USE_STUDIO is True, running veadk studio"
@@ -54,10 +57,15 @@ elif [ "$USE_STUDIO" = "False" ]; then
5457
cd ../
5558
exec python3 -m veadk.cli.main web --host "0.0.0.0"
5659
else
57-
echo "USE_ADK_WEB is False, running a2a server"
58-
exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio
60+
if [ "$USE_MCP" = "True" ]; then
61+
echo "USE_MCP is True, running MCP server"
62+
exec python3 app_mcp.py --transport http --host $HOST --port $PORT --log-level "INFO"
63+
else
64+
echo "USE_MCP is False, running a2a server"
65+
exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio
66+
fi
5967
fi
6068
else
61-
# running a2a server
69+
# running a2a server (default)
6270
exec python3 -m uvicorn app:app --host $HOST --port $PORT --timeout-graceful-shutdown $TIMEOUT --loop asyncio
6371
fi

veadk/cloud/cloud_agent_engine.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def deploy(
9393
gateway_upstream_name: str = "",
9494
use_studio: bool = False,
9595
use_adk_web: bool = False,
96+
use_mcp: bool = False,
9697
) -> CloudApp:
9798
"""Deploy local agent project to Volcengine FaaS platform.
9899
@@ -128,6 +129,15 @@ def deploy(
128129

129130
veadk.config.veadk_environments["USE_ADK_WEB"] = "False"
130131

132+
if use_mcp:
133+
import veadk.config
134+
135+
veadk.config.veadk_environments["USE_MCP"] = "True"
136+
else:
137+
import veadk.config
138+
139+
veadk.config.veadk_environments["USE_MCP"] = "False"
140+
131141
# convert `path` to absolute path
132142
path = str(Path(path).resolve())
133143
self._prepare(path, application_name)

veadk/tracing/telemetry/opentelemetry_tracer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def _init_tracer_provider(self):
112112

113113
if not isinstance(global_tracer_provider, TracerProvider):
114114
logger.info(
115-
f"Global tracer provider has not been set. Create tracer provider and set it now."
115+
"Global tracer provider has not been set. Create tracer provider and set it now."
116116
)
117117
# 1.1 init tracer provider
118118
tracer_provider = trace_sdk.TracerProvider()
@@ -144,7 +144,6 @@ def _init_tracer_provider(self):
144144
self._processors.append(processor)
145145
logger.debug(f"Init OpentelemetryTracer with {len(self.exporters)} exporters.")
146146

147-
148147
@override
149148
def dump(
150149
self,

0 commit comments

Comments
 (0)