Skip to content

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

  1. MCP in FCC Overview
  2. MCPToolDefinition
  3. Creating a Tool Factory Function
  4. Registering Tools in MCPToolRegistry
  5. MCPResourceDefinition
  6. URI Templates and Matching
  7. MCPPromptDefinition
  8. Adding Prompts to MCPPromptRegistry
  9. End-to-End Example: Quality Gate Check Tool
  10. Testing
  11. 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

for tool in registry.list_tools():
    print(f"  {tool.name}: {tool.description}")

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"]