Skip to main content

Muscles MCP

Model Context Protocol projection for Muscles.

This package exposes a Muscles application to AI tools through MCP without copying application logic into the MCP layer.

Installation

pip install muscles-mcp

The package requires the published muscles core and can be combined with muscles-asgi for MCP-over-HTTP deployments.

  • muscles - core actions, inspect contract, dispatcher and canonical documentation.
  • muscles-ai - AI/RAG actions that can be exposed as MCP tools.
  • muscles-documents - document actions that can be exposed to AI tools.
  • muscles-asgi - ASGI entrypoint binding for MCP-over-HTTP scenarios.
  • muscles-benchmarks - MCP projection regression checks.

Concept Guardrails

  • Muscles remains the source of truth for actions, schemas, rules, context, permissions, and execution.
  • MCP tools/resources must be generated from inspect_application(app).
  • The MCP layer must not invent its own routing, validation, auth, action registry, or business model.
  • A use case implemented once in Muscles should become available through MCP without rewriting the use case.
  • The standalone JSON-RPC server owns only protocol shape, OAuth discovery metadata, and serialization. Tool execution, authorization, token storage, audit, and domain payload mapping remain in the host application.
  • Machine-readable metadata is a product feature, not an internal detail.
  • MCP is a protocol projection over ApplicationMeta, Context, the application-scoped registry, ActionContract, and ActionDispatcher; it is not a separate runtime next to Muscles.

Implemented Features

Implemented MCP projection as a Muscles application strategy:

  • McpStrategy подключается через Context(McpStrategy, transport=...):
    • transport=asgi — MCP-контекст привязывается к ASGI entrypoint-контексту;
    • transport=<asgi_context_name> или transport=<asgi_context_obj> — явная связка с конкретным entrypoint;
    • transport="mcp" — опциональный fallback для сценариев совместимости (обычно не нужен).
  • legacy McpRouter was removed from the Muscles strategy API: register Muscles actions through the core @app.action decorator with metadata["mcp"].
  • McpServer is available as a standalone JSON-RPC protocol adapter for applications that already own their business tool/resource registry and need MCP-compatible transport responses.
  • McpAdapter remains a compatibility facade over the same strategy logic;
  • list_tools() from contract actions;
  • list_resources() for canonical MCP URIs:
    • muscles://app/inspect
    • muscles://app/actions
    • muscles://app/routes
    • muscles://app/schemas
    • muscles://app/rules
  • read_resource(uri) returns stable JSON payload per resource;
  • call_tool() delegates to Muscles core ActionDispatcher with transport="mcp" (no business-logic copy);
  • tool input validation is performed by Muscles core;
  • permission/rule denial is returned as structured MCP error mapped from core errors.
  • MCP protocol messages are represented by Muscles-based models in muscles.mcp.schema.mcp;
  • MCP schema module and class names are protocol-specific and do not reuse core names such as schema.py, model.py, response.py, Model, Schema, or Response.
  • standalone JSON-RPC support includes initialize, ping, tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, batch requests, notifications, structured content normalization, JSON-RPC error mapping, and OAuth/DCR discovery helpers.

Run tests

python -m pytest -q

User docs:

Runnable example:

Standalone JSON-RPC Layer

Use McpServer when a service already has its own business layer and needs only MCP protocol handling. The server receives JSON-RPC messages and delegates business work to callbacks supplied by the host application.

from muscles.mcp import McpResource, McpServer, McpTool, mcp_list_schema


server = McpServer(
    name="assetforge-mcp",
    version="1.0.0",
    instructions="Use AssetForge tools with the connected user's permissions.",
    tools=[
        McpTool(
            name="workspaces.list",
            description="List workspaces available to the current user.",
            input_schema={"type": "object", "properties": {}},
            output_schema=mcp_list_schema(),
            read_only=True,
        )
    ],
    resources=[
        McpResource(
            uri="assetforge://catalog",
            name="catalog",
            description="Available AssetForge MCP tools and resources.",
        )
    ],
    call_tool=lambda name, arguments, context: [{"uid": "workspace-full-uid"}],
    read_resource=lambda uri, arguments, context: {"uri": uri},
)

response = server.handle_jsonrpc(
    {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {"name": "workspaces.list", "arguments": {}},
    }
)

Tool results are normalized so structuredContent is always an object:

  • dict values are returned as-is;
  • list values become {"items": [...], "count": N};
  • primitive values and None become {"value": ...}.

The same object is serialized into content[0].text, which keeps the response compatible with clients that read either field.

register_mcp_routes(...) can register OAuth discovery endpoints and a streamable HTTP POST route around a McpServer. OAuth client storage, authorization codes, token issuing, permission checks, and audit logging stay in the host application through provider/callback boundaries.

Detailed Usage Example

This example shows the intended architecture:

  • Muscles owns the application contract and business execution.
  • muscles-mcp exposes that contract as MCP tools and resources.
  • The adapter does not copy use cases, permissions, routes, or validation rules.

1. Describe an action in the Muscles contract

In a real application this contract should come from inspect_application(app) or muscles inspect --json. The important part is that the action is described once by Muscles and then reused by MCP.

contract = {
    "contract_version": "1",
    "framework": "Muscles",
    "app": "BookingApp",
    "actions": [
        {
            "name": "bookings.create",
            "description": "Create a booking request",
            "input_schema": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "guest_count": {"type": "integer"},
                },
                "required": ["title"],
            },
        }
    ],
    "routes": [
        {
            "name": "bookings.create",
            "path": "/bookings",
            "method": "POST",
        }
    ],
    "schemas": [
        {
            "name": "BookingCreate",
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "guest_count": {"type": "integer"},
            },
        }
    ],
    "rules": [
        {"name": "bookings.public_create"}
    ],
}

2. Connect MCP to the existing Muscles execution path

The Muscles application is the bridge. The preferred integration is a strategy connected to the Muscles context. MCP reads the contract through inspect_application(app) and executes tools through ActionDispatcher.

from muscles.mcp import McpAdapter, McpStrategy
from muscles import ApplicationMeta, Context
from muscles.asgi import AsgiStrategy


class BookingApp(metaclass=ApplicationMeta):
    asgi = Context(AsgiStrategy)
    mcp_context = Context(McpStrategy, transport=asgi)

    # Example with explicit binding without router in MCP context params:
    # asgi_admin = Context(AsgiStrategy, params={"profile": "admin"})
    # mcp_admin = Context(McpStrategy, transport=asgi_admin, params={"mcp_profile": "admin"})


app = BookingApp()


@app.action(
    name="bookings.create",
    description="Create a booking request",
    input_schema={
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "guest_count": {"type": "integer"},
        },
        "required": ["title"],
    },
    transports=["mcp"],
    metadata={
        "mcp": {
            "route": "/create",
            "full_route": "/bookings/create",
            "name": "bookings.create",
            "transport": "mcp",
            "server": "public",
            "servers": ["public"],
            "token": "SIMSIM-PUBLIC",
        }
    },
)
def create_booking(payload, context):
    return {
        "id": 1,
        "title": payload["title"],
        "guest_count": payload.get("guest_count", 1),
        "status": "created",
        "transport": context.transport,
    }


tools = app.mcp_context.execute(operation="list_tools", server="public", token="SIMSIM-PUBLIC")
admin_tools = app.mcp_context.execute(operation="list_tools", server="admin")
response = app.mcp_context.execute(
    operation="call_tool",
    server="public",
    token="SIMSIM-PUBLIC",
    name="bookings.create",
    arguments={"title": "Discovery call"},
)

# Compatibility facade for existing callers.
adapter = McpAdapter.from_application(app)

See examples/booking_app.py for a complete application example that uses a Muscles Model as the action input schema. MCP now normalizes Model-based schemas during execution automatically. If you need a standalone schema builder, use build_model_json_schema.

2.5. One app for MCP, ASGI and WSGI

A single App instance can power MCP, ASGI and WSGI entrypoints. Use make_protocol_app(app, protocol) to switch protocol handling in one place. The same application context, registry, actions, routes, and validation logic stay as the single source of truth.

For MCP over HTTP/WSGI/CLI transport (без переключения бизнес-контекста) use:

from muscles.mcp import make_protocol_app

mcp_http = make_protocol_app(app, "mcp-asgi", route="/mcp")
mcp_wsgi = make_protocol_app(app, "mcp-wsgi", route="/mcp")
mcp_cli = make_protocol_app(app, "mcp-cli")

mcp_asgi/wsgi accept the same MCP payload (JSON):

{
  "operation": "call_tool",
  "name": "bookings.create",
  "arguments": {"title": "Booking", "guest_count": 2},
  "server": "public",
  "token": "SIMSIM-PUBLIC"
}

mcp_cli executes one request from dict/JSON and prints MCP response.

3. Let an AI client discover available tools

tools = app.mcp_context.execute(operation="list_tools")

assert tools == [
    {
        "name": "bookings.create",
        "description": "Create a booking request",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "guest_count": {"type": "integer"},
            },
            "required": ["title"],
        },
    }
]

An AI client should use this list instead of guessing which functions exist in the codebase.

4. Expose Muscles metadata as MCP resources

resources = app.mcp_context.execute(operation="list_resources")

assert {resource["uri"] for resource in resources} == {
    "muscles://app/inspect",
    "muscles://app/actions",
    "muscles://app/routes",
    "muscles://app/schemas",
    "muscles://app/rules",
}

inspect_resource = app.mcp_context.execute(operation="read_resource", uri="muscles://app/inspect")
actions_resource = app.mcp_context.execute(operation="read_resource", uri="muscles://app/actions")

These resources give an agent stable context before it edits or calls the app. The agent can inspect routes, actions, schemas, and rules through one official contract instead of scanning random Python files.

5. Call a Muscles action through MCP

response = app.mcp_context.execute(
    operation="call_tool",
    name="bookings.create",
    arguments={"title": "Discovery call", "guest_count": 2},
)

assert response == {
    "content": [
        {
            "type": "json",
            "json": {
                "id": 1,
                "title": "Discovery call",
                "guest_count": 2,
                "status": "created",
            },
        }
    ]
}

The MCP adapter does not know how to create a booking. It delegates execution to the Muscles dispatcher, where validation, rules and the use case live.

6. Validation and permission errors stay structured

If a required argument is missing, the adapter returns a machine-readable error:

missing_title = adapter.call_tool("bookings.create", {"guest_count": 2})

assert missing_title == {
    "isError": True,
    "error": {
        "code": "invalid_params",
        "message": "'title' is a required property",
        "data": {"path": []},
    },
}

If the Muscles rules/security layer denies the action, the core dispatcher raises ActionPermissionDenied and MCP maps it to a protocol error:

from muscles import ActionPermissionDenied


def denied_handler(payload, context):
    raise ActionPermissionDenied(context.action.name, "Denied by Muscles rules")


app.action(name="bookings.create")(denied_handler)
secure_adapter = McpAdapter.from_application(app)
denied = secure_adapter.call_tool("bookings.create", {"title": "Call"})

assert denied == {
    "isError": True,
    "error": {
        "code": "permission_denied",
        "message": "Denied by Muscles rules",
        "data": None,
    },
}

This keeps MCP aligned with the framework: security decisions belong to Muscles, while MCP only transports the structured result.

7. Build from a real Muscles application

When the app already supports inspect_application(app), use McpAdapter.from_application(app):

from muscles.mcp import McpAdapter

adapter = McpAdapter.from_application(app)

tools = adapter.list_tools()
inspect_resource = adapter.read_resource("muscles://app/inspect")
result = adapter.call_tool("bookings.create", {"title": "Call"})

from_application() delegates tool execution to ActionDispatcher with transport="mcp". The application model, rules, schemas and use cases stay in Muscles.

Metadata

Release files for muscles-mcp 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for muscles-mcp 2.0.0
File Size Uploaded Provenance Actions
muscles_mcp-2.0.0.tar.gz 34.0 kB With provenance Details for muscles_mcp-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for muscles-mcp 2.0.0
File Interpreter ABI Platform Actions
muscles_mcp-2.0.0-py3-none-any.whl Python 3 none any Details for muscles_mcp-2.0.0-py3-none-any.whl

Total release size: 56.1 kB

Release files / muscles_mcp-2.0.0.tar.gz

Download URL muscles_mcp-2.0.0.tar.gz
Size 34.0 kB
Tags Source
SHA-256 checksum
How to use checksums
473079469637405d10b365322a6f56de1cd2ab7f0e30b0d6d8c59f26340ea270
BLAKE2b-256 checksum
How to use checksums
9775d22aaa0f889fb63a67530d5276b190aa470517fb85957ebc8335698c5f3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / muscles_mcp-2.0.0-py3-none-any.whl

Download URL muscles_mcp-2.0.0-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eb40cf03db32667ad4f46fb0c4b511daa3fe71aaa1a193791063c47ed3812c40
BLAKE2b-256 checksum
How to use checksums
013c66c450fd23caf81398bb8f3a3c08e611000dee41c84acb5f3ae39d7921fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for muscles-mcp 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for muscles_mcp-2.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page