Custom MCP Tools¶
This guide covers how to create, register, and test custom MCP (Model Context Protocol) tools, resources, and prompts within the FCC framework. All MCP primitives are defined as frozen dataclasses and managed through registry classes.
Table of Contents¶
- MCP in FCC Overview
- MCPToolDefinition
- Creating a Tool Factory Function
- Registering Tools in MCPToolRegistry
- MCPResourceDefinition
- URI Templates and Matching
- MCPPromptDefinition
- Adding Prompts to MCPPromptRegistry
- End-to-End Example: Quality Gate Check Tool
- Testing
- Related Documentation
MCP in FCC Overview¶
The FCC MCP layer is implemented in src/fcc/protocols/mcp/ and consists of
three primitive types:
| Primitive | Module | Default Count | Registry Class |
|---|---|---|---|
| Tools | tools.py |
14 | MCPToolRegistry |
| Resources | resources.py |
14 | MCPResourceRegistry |
| Prompts | prompts.py |
6 | MCPPromptRegistry |
These primitives are served by FccMcpServer (in server.py) or the JSON-RPC
fallback server (in fallback.py).
MCPToolDefinition¶
A tool definition describes a callable operation with a JSON Schema input specification.
from fcc.protocols.mcp.tools import MCPToolDefinition
tool = MCPToolDefinition(
name="fcc_my_tool",
description="Perform a custom operation on FCC data.",
input_schema={
"type": "object",
"properties": {
"persona_id": {
"type": "string",
"description": "Target persona ID.",
},
"depth": {
"type": "integer",
"description": "Analysis depth (1-5).",
"minimum": 1,
"maximum": 5,
"default": 3,
},
},
"required": ["persona_id"],
},
)
Fields¶
| Field | Type | Description |
|---|---|---|
name |
str |
Unique tool name. Convention: prefix with fcc_. |
description |
str |
Human-readable description of what the tool does. |
input_schema |
dict[str, Any] |
JSON Schema describing the tool's input parameters. |
Serialization¶
tool_dict = tool.to_dict()
restored = MCPToolDefinition.from_dict(tool_dict)
assert restored.name == tool.name
Creating a Tool Factory Function¶
The default tools are created via factory functions -- parameterless functions
that return a MCPToolDefinition. Follow this pattern for custom tools:
def _tool_fcc_quality_check() -> MCPToolDefinition:
"""Tool: run a quality gate check against a deliverable."""
return MCPToolDefinition(
name="fcc_quality_check",
description="Run quality gate checks against a deliverable artifact.",
input_schema={
"type": "object",
"properties": {
"deliverable_id": {
"type": "string",
"description": "Identifier of the deliverable to check.",
},
"gate_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of gate IDs to evaluate. Omit for all gates.",
},
"strict": {
"type": "boolean",
"description": "If true, fail on any gate below threshold.",
"default": False,
},
},
"required": ["deliverable_id"],
},
)
Naming Conventions¶
- Tool names should be prefixed with
fcc_to avoid collisions. - Use snake_case:
fcc_my_custom_tool. - Keep names concise but descriptive.
Registering Tools in MCPToolRegistry¶
Default Registry¶
The MCPToolRegistry.create_default() factory creates a registry pre-loaded
with all 14 built-in tools:
from fcc.protocols.mcp.tools import MCPToolRegistry
registry = MCPToolRegistry.create_default()
print(registry.count()) # 14
Adding a Custom Tool¶
registry = MCPToolRegistry.create_default()
custom_tool = _tool_fcc_quality_check()
registry.register(custom_tool)
print(registry.count()) # 15
Lookup¶
tool = registry.get("fcc_quality_check")
assert tool is not None
assert tool.description.startswith("Run quality gate")
Listing All Tools¶
Duplicate Prevention¶
Attempting to register a tool with a name that already exists raises
ValueError:
try:
registry.register(custom_tool) # Already registered
except ValueError as exc:
print(exc) # "Tool already registered: fcc_quality_check"
MCPResourceDefinition¶
Resources expose FCC data endpoints via fcc:// URIs. They can be exact
URIs or URI templates with {param} placeholders.
from fcc.protocols.mcp.resources import MCPResourceDefinition
# Exact URI
resource = MCPResourceDefinition(
uri="fcc://quality-gates",
name="Quality Gates",
description="All configured quality gates with thresholds and criteria.",
)
# URI template
template_resource = MCPResourceDefinition(
uri="fcc://quality-gates/{gate_id}",
name="Quality Gate Detail",
description="Get a specific quality gate by ID.",
)
Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
uri |
str |
-- | Resource URI or URI template. |
name |
str |
-- | Human-readable name. |
description |
str |
-- | What the resource provides. |
mime_type |
str |
"application/json" |
MIME type of content. |
URI Templates and Matching¶
Resources with {param} syntax in their URI are templates.
resource = MCPResourceDefinition(
uri="fcc://personas/{id}/dimensions",
name="Persona Dimensions",
description="Get the 9-category, 56-dimension profile for a persona.",
)
# Check if it is a template
assert resource.is_template is True
# Match a concrete URI
params = resource.matches("fcc://personas/RC/dimensions")
assert params == {"id": "RC"}
# Non-matching URI returns None
assert resource.matches("fcc://workflows") is None
MCPResourceRegistry¶
from fcc.protocols.mcp.resources import MCPResourceRegistry
registry = MCPResourceRegistry.create_default()
print(registry.count()) # 14
# Exact lookup
res = registry.get("fcc://personas")
assert res is not None
# Template matching
res = registry.match("fcc://personas/RC")
assert res is not None
assert res.uri == "fcc://personas/{id}"
# Match with parameter extraction
res, params = registry.match_with_params("fcc://personas/RC/dimensions")
assert params == {"id": "RC"}
Registering Custom Resources¶
registry.register(MCPResourceDefinition(
uri="fcc://quality-gates/{gate_id}/history",
name="Gate History",
description="Historical pass/fail results for a quality gate.",
))
MCPPromptDefinition¶
Prompts are template strings with {arg} placeholders. When rendered, they
produce {"role": "user", "content": "..."} messages suitable for LLM
consumption.
from fcc.protocols.mcp.prompts import MCPPromptDefinition
prompt = MCPPromptDefinition(
name="quality_assessment",
description="Assess the quality of a deliverable against FCC standards.",
arguments=(
{
"name": "deliverable_type",
"description": "Type of deliverable (e.g. code, document, design).",
"required": True,
},
{
"name": "criteria",
"description": "Comma-separated evaluation criteria.",
"required": False,
"default": "completeness,accuracy,clarity",
},
),
template=(
"Assess the quality of the following {deliverable_type}. "
"Apply these evaluation criteria: {criteria}. "
"Provide a structured assessment with scores and recommendations."
),
)
Rendering¶
message = prompt.render(deliverable_type="API design document")
# message: {
# "role": "user",
# "content": "Assess the quality of the following API design document. ..."
# }
Optional arguments use their defaults when omitted. Required arguments raise
ValueError if missing.
Adding Prompts to MCPPromptRegistry¶
from fcc.protocols.mcp.prompts import MCPPromptRegistry
registry = MCPPromptRegistry.create_default()
print(registry.count()) # 6
registry.register(prompt)
print(registry.count()) # 7
# Lookup and render
p = registry.get("quality_assessment")
msg = p.render(deliverable_type="test plan", criteria="coverage,edge_cases")
End-to-End Example: Quality Gate Check Tool¶
This example shows how to add a custom quality gate check as an MCP tool, a corresponding resource, and a prompt.
1. Define the tool¶
def _tool_fcc_quality_gate_check() -> MCPToolDefinition:
return MCPToolDefinition(
name="fcc_quality_gate_check",
description="Evaluate a deliverable against specific quality gates.",
input_schema={
"type": "object",
"properties": {
"deliverable_id": {"type": "string"},
"gates": {
"type": "array",
"items": {"type": "string"},
"description": "Gate IDs to check. Empty = all gates.",
},
},
"required": ["deliverable_id"],
},
)
2. Define the resource¶
gate_result_resource = MCPResourceDefinition(
uri="fcc://quality-gate-results/{deliverable_id}",
name="Quality Gate Results",
description="Retrieve quality gate evaluation results for a deliverable.",
)
3. Define the prompt¶
gate_prompt = MCPPromptDefinition(
name="quality_gate_review",
description="Review quality gate results and recommend next actions.",
arguments=(
{"name": "deliverable_id", "required": True,
"description": "The deliverable that was evaluated."},
{"name": "failed_gates", "required": True,
"description": "Comma-separated list of failed gate IDs."},
),
template=(
"Review the quality gate results for deliverable {deliverable_id}. "
"The following gates failed: {failed_gates}. "
"Analyze root causes and recommend corrective actions."
),
)
4. Register all three¶
from fcc.protocols.mcp.tools import MCPToolRegistry
from fcc.protocols.mcp.resources import MCPResourceRegistry
from fcc.protocols.mcp.prompts import MCPPromptRegistry
tool_reg = MCPToolRegistry.create_default()
tool_reg.register(_tool_fcc_quality_gate_check())
res_reg = MCPResourceRegistry.create_default()
res_reg.register(gate_result_resource)
prompt_reg = MCPPromptRegistry.create_default()
prompt_reg.register(gate_prompt)
Testing¶
CLI Integration Testing¶
Use Click's CliRunner to test MCP commands end-to-end:
from click.testing import CliRunner
from fcc.scaffold.cli import cli
def test_protocol_tools_command():
runner = CliRunner()
result = runner.invoke(cli, ["protocol", "tools"])
assert result.exit_code == 0
assert "fcc_simulate" in result.output
Registry Verification¶
def test_default_tool_count():
registry = MCPToolRegistry.create_default()
assert registry.count() == 14
def test_all_tools_have_descriptions():
registry = MCPToolRegistry.create_default()
for tool in registry.list_tools():
assert tool.description, f"Tool {tool.name} has no description"
def test_resource_template_matching():
registry = MCPResourceRegistry.create_default()
res, params = registry.match_with_params("fcc://personas/SQC")
assert res is not None
assert params == {"id": "SQC"}
def test_prompt_rendering():
registry = MCPPromptRegistry.create_default()
prompt = registry.get("fcc_find")
msg = prompt.render(topic="distributed systems")
assert "distributed systems" in msg["content"]
assert msg["role"] == "user"
Custom Tool Validation¶
def test_custom_tool_schema():
tool = _tool_fcc_quality_gate_check()
schema = tool.input_schema
assert schema["type"] == "object"
assert "deliverable_id" in schema["properties"]
assert "deliverable_id" in schema["required"]
Related Documentation¶
- Protocol Bridge Patterns -- routing messages through bridges
- Extending A2A Agent Cards -- persona-to-card mapping
- WebSocket Architecture -- real-time transport layer
- Understanding the FCC Ecosystem -- project overview