Skip to content
Open
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
3 changes: 2 additions & 1 deletion requirements/test.in
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ fastsafetensors>=0.1.10
pydantic>=2.12 # 2.11 leads to error on python 3.13
decord==0.6.0
terratorch @ git+https://github.com/IBM/terratorch.git@1.1.rc3 # required for PrithviMAE test
gpt-oss >= 0.0.7; python_version > '3.11'
gpt-oss >= 0.0.7; python_version > '3.11'
fastmcp # required for MCP tests
2 changes: 2 additions & 0 deletions tests/entrypoints/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
70 changes: 70 additions & 0 deletions tests/entrypoints/mcp/test_mcp_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for MCP utils."""

from openai.types.responses.tool import (
CodeInterpreter,
CodeInterpreterContainerCodeInterpreterToolAuto,
FunctionTool,
Mcp,
WebSearchPreviewTool,
)

from vllm.entrypoints.mcp.mcp_utils import normalize_tool_to_mcp


def test_normalize_mcp_tool_passthrough():
"""MCP tools should pass through unchanged."""
mcp_tool = Mcp(
type="mcp", server_label="weather", server_url="http://localhost:8765/sse"
)
result = normalize_tool_to_mcp(mcp_tool)
assert result == mcp_tool
assert result.server_label == "weather"
assert result.server_url == "http://localhost:8765/sse"


def test_normalize_code_interpreter():
"""CodeInterpreter should convert to MCP with server_label='code_interpreter'."""
# For test purposes we provide a minimal container (required by Pydantic)
# Just testing that type is correctly converted
code_tool = CodeInterpreter(
type="code_interpreter",
container=CodeInterpreterContainerCodeInterpreterToolAuto(type="auto"),
)
result = normalize_tool_to_mcp(code_tool)

assert isinstance(result, Mcp)
assert result.type == "mcp"
assert result.server_label == "code_interpreter"
# Container field is intentionally discarded


def test_normalize_web_search_preview():
"""WebSearchPreviewTool should convert to MCP with server_label='browser'."""
search_tool = WebSearchPreviewTool(
type="web_search_preview",
search_context_size="medium",
)
result = normalize_tool_to_mcp(search_tool)

assert isinstance(result, Mcp)
assert result.type == "mcp"
assert result.server_label == "browser"
# search_context_size is intentionally discarded


def test_normalize_other_tools_passthrough():
"""Other tool types should pass through unchanged."""
# Using a FunctionTool as an example of a non-converted tool type
function_tool = FunctionTool(
type="function",
name="test_func",
function={"name": "test_func", "description": "Test function"},
)

result = normalize_tool_to_mcp(function_tool)

# Should be unchanged
assert result == function_tool
assert result.type == "function"
183 changes: 183 additions & 0 deletions tests/entrypoints/openai/memory_mcp_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Standalone Memory MCP Server
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this file should go in the mcp folder


This is a standalone MCP server that provides memory storage capabilities
with isolation based on x-memory-id headers.

Tools provided:
- store: Store a key-value pair in memory
- retrieve: Retrieve a value by key
- list_keys: List all keys in the current memory space
- delete: Delete a key from memory

Run standalone:
python memory_mcp_server.py --port 8765
"""

import argparse
import os
import socket
import subprocess
import sys
import time

from fastmcp import Context, FastMCP

# In-memory storage: {memory_id: {key: value}}
memories: dict[str, dict[str, str]] = {}

# Default memory space for requests without x-memory-id header
DEFAULT_MEMORY_ID = "default"

# Create FastMCP app
mcp = FastMCP("memory")


def extract_memory_id(ctx: Context) -> str:
"""Extract memory_id from request context headers."""
try:
# Try to get HTTP request from context
http_request = ctx.get_http_request()
if http_request and hasattr(http_request, "headers"):
headers = http_request.headers
# Headers may be case-insensitive, check variations
memory_id = headers.get("x-memory-id") or headers.get("X-Memory-Id")
if memory_id:
return memory_id
except Exception:
pass

return DEFAULT_MEMORY_ID


@mcp.tool()
def store(key: str, value: str, ctx: Context) -> str:
"""Store a key-value pair in memory.

Args:
key: The key to store
value: The value to store
"""
memory_id = extract_memory_id(ctx)

# Ensure memory space exists
if memory_id not in memories:
memories[memory_id] = {}

memories[memory_id][key] = value
return (
f"Successfully stored key '{key}' with value '{value}' "
f"in memory space '{memory_id}'"
)


@mcp.tool()
def retrieve(key: str, ctx: Context) -> str:
"""Retrieve a value by key from memory.

Args:
key: The key to retrieve
"""
memory_id = extract_memory_id(ctx)

# Ensure memory space exists
if memory_id not in memories:
memories[memory_id] = {}

value = memories[memory_id].get(key)
if value is None:
return f"Key '{key}' not found in memory space '{memory_id}'"
return f"Retrieved value for key '{key}': {value}"


@mcp.tool()
def list_keys(ctx: Context) -> str:
"""List all keys in the current memory space."""
memory_id = extract_memory_id(ctx)

# Ensure memory space exists
if memory_id not in memories:
memories[memory_id] = {}

keys = list(memories[memory_id].keys())
if not keys:
return f"No keys found in memory space '{memory_id}'"
return f"Keys in memory space '{memory_id}': {', '.join(keys)}"


@mcp.tool()
def delete(key: str, ctx: Context) -> str:
"""Delete a key from memory.

Args:
key: The key to delete
"""
memory_id = extract_memory_id(ctx)

# Ensure memory space exists
if memory_id not in memories:
memories[memory_id] = {}

if key in memories[memory_id]:
del memories[memory_id][key]
return f"Successfully deleted key '{key}' from memory space '{memory_id}'"
return f"Key '{key}' not found in memory space '{memory_id}'"


def start_test_server(port: int) -> subprocess.Popen:
"""Start memory MCP server for testing.

Args:
port: Port to run server on

Returns:
subprocess.Popen object for the running server
"""
script_path = os.path.abspath(__file__)
process = subprocess.Popen(
[sys.executable, script_path, "--port", str(port)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)

# Wait for server to be ready (TCP check)
for _ in range(30):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
result = sock.connect_ex(("localhost", port))
sock.close()
if result == 0:
return process
except Exception:
pass
time.sleep(0.1)

# Failed to start
process.kill()
stdout, stderr = process.communicate()
raise RuntimeError(
f"Memory MCP server failed to start.\n"
f"stdout: {stdout.decode()}\nstderr: {stderr.decode()}"
)


def main():
parser = argparse.ArgumentParser(description="Memory MCP Server")
parser.add_argument(
"--port",
type=int,
default=8765,
help="Port to run the server on (default: 8765)",
)
args = parser.parse_args()

print(f"Starting Memory MCP Server on port {args.port}...")
mcp.run(port=args.port, transport="sse")


if __name__ == "__main__":
main()
Loading