Add M7.3 CmdForge as MCP server with FastMCP, expose/deny policy, and schema mapping

This commit is contained in:
rob
2026-07-20 00:24:54 -03:00
parent dee48d8cea
commit 3525e2eeae
5 changed files with 336 additions and 0 deletions
+97
View File
@@ -5,6 +5,7 @@ import time
from pathlib import Path
import pytest
from unittest.mock import patch, MagicMock
from cmdforge.mcp_client import (
DEFAULT_INHERITED_ENV,
McpServerConfig,
@@ -397,3 +398,99 @@ class TestMcpCli:
monkeypatch.setattr(sys, "argv", ["cmdforge", "mcp"])
assert main() == 0
assert "No MCP servers configured" in capsys.readouterr().out
class TestMcpServeConfig:
def test_empty_expose_exposes_nothing(self):
from cmdforge.mcp_client import McpServeConfig
config = McpServeConfig(expose=[], deny=[])
assert not config.is_exposed("summarize")
def test_wildcard_exposes_all_public(self):
from cmdforge.mcp_client import McpServeConfig
config = McpServeConfig(expose=["*"], deny=[])
assert config.is_exposed("summarize")
assert config.is_exposed("any-tool")
def test_deny_overrides_expose(self):
from cmdforge.mcp_client import McpServeConfig
config = McpServeConfig(expose=["*"], deny=["dangerous"])
assert config.is_exposed("summarize")
assert not config.is_exposed("dangerous")
def test_prefix_deny(self):
from cmdforge.mcp_client import McpServeConfig
config = McpServeConfig(expose=["*"], deny=["internal-*"])
assert not config.is_exposed("internal-cleanup")
assert not config.is_exposed("internal-")
assert config.is_exposed("public-tool")
def test_explicit_expose(self):
from cmdforge.mcp_client import McpServeConfig
config = McpServeConfig(expose=["summarize", "extract"])
assert config.is_exposed("summarize")
assert config.is_exposed("extract")
assert not config.is_exposed("other")
class TestToolNameMapping:
def test_simple_name_passes_through(self):
from cmdforge.mcp_server import _cmdforge_tool_name
assert _cmdforge_tool_name("summarize") == "summarize"
def test_qualified_name_uses_double_underscore(self):
from cmdforge.mcp_server import _cmdforge_tool_name
assert _cmdforge_tool_name("owner/tool") == "owner__tool"
class TestArgumentToJsonSchema:
def test_string_arg(self):
from cmdforge.mcp_server import _argument_to_json_schema
from cmdforge.tool import ToolArgument
arg = ToolArgument(flag="--name", variable="name", description="The name")
schema = _argument_to_json_schema(arg)
assert schema["type"] == "string"
assert schema["description"] == "The name"
def test_integer_arg_with_default(self):
from cmdforge.tool import ToolArgument
from cmdforge.mcp_server import _argument_to_json_schema
arg = ToolArgument(flag="--limit", variable="limit", default="10")
setattr(arg, "type", "integer")
schema = _argument_to_json_schema(arg)
assert schema["type"] == "integer"
assert schema["default"] == "10"
def test_arg_with_enum(self):
from cmdforge.tool import ToolArgument
from cmdforge.mcp_server import _argument_to_json_schema
arg = ToolArgument(flag="--mode", variable="mode")
setattr(arg, "enum", ["fast", "accurate"])
schema = _argument_to_json_schema(arg)
assert schema["enum"] == ["fast", "accurate"]
class TestBuildToolSchema:
def test_builds_input_schema(self, tmp_path):
from cmdforge.tool import Tool, ToolArgument, save_tool
from cmdforge.mcp_server import _build_tool_schema
with patch('cmdforge.tool.TOOLS_DIR', tmp_path / ".cmdforge"):
tool = Tool(
name="greet",
description="Greet someone",
arguments=[
ToolArgument(flag="--name", variable="name",
description="Who to greet", default="World"),
ToolArgument(flag="--count", variable="count", default="1"),
],
output="Hello {name} x{count}\n",
)
schema = _build_tool_schema(tool)
assert schema["name"] == "greet"
assert "Greet" in schema["description"]
assert schema["inputSchema"]["type"] == "object"
assert "name" in schema["inputSchema"]["properties"]
assert "count" in schema["inputSchema"]["properties"]