GraphQL MCP Examples

Each example is a standalone GraphQL MCP server. Open GraphiQL to explore the schema interactively, or connect to the MCP endpoint from any MCP-compatible client.

Hello World

Minimal MCP server with a single query — the simplest possible starting point.

Task Manager

Full CRUD with enums, mutations, UUID/datetime scalars, and in-memory state.

Nested API

Nested tools, @mcp directive, Pydantic models, and async resolvers.

Remote API

Wraps a public GraphQL API (Countries) as MCP tools via from_remote_url().

graphql-api

Expose a graphql-api app as MCP tools, customized with the @mcp directive.

graphql-core

Expose a graphql-core schema as MCP tools, customized with the @mcp directive.

Ariadne

Expose a schema-first Ariadne API as MCP tools, customized with the @mcp directive.

Strawberry

Expose a Strawberry API as MCP tools, customized with the @mcp directive.

Graphene

Expose a Graphene API as MCP tools, customized with the @mcp directive.

Hello World

Minimal MCP server with a single query — the simplest possible starting point.

hello_world.py
"""Hello World - minimal GraphQL MCP server example."""

from graphql_api import GraphQLAPI, field
from graphql_mcp.server import GraphQLMCP


class HelloWorldAPI:

    @field
    def hello(self, name: str = "World") -> str:
        """Say hello to someone."""
        return f"Hello, {name}!"


api = GraphQLAPI(root_type=HelloWorldAPI)
server = GraphQLMCP.from_api(api, graphql_http_kwargs={
    "graphiql_example_query": """\
{
  hello

  custom: hello(name: "GraphQL MCP")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8002)

Task Manager

Full CRUD with enums, mutations, UUID/datetime scalars, and in-memory state.

task_manager.py
"""Task Manager - CRUD example with enums, mutations, and in-memory state.

Demonstrates:
- Dataclass types as GraphQL object types
- Enums (Priority, Status)
- Queries with optional filters
- Mutations via @field(mutable=True)
- UUID and datetime scalars
- Optional fields and list types
"""

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional
from uuid import UUID, uuid4

from graphql_api import GraphQLAPI, field
from graphql_mcp.server import GraphQLMCP


class Priority(str, Enum):
    LOW = "LOW"
    MEDIUM = "MEDIUM"
    HIGH = "HIGH"
    CRITICAL = "CRITICAL"


class Status(str, Enum):
    TODO = "TODO"
    IN_PROGRESS = "IN_PROGRESS"
    DONE = "DONE"


@dataclass
class Task:
    id: UUID
    title: str
    description: str
    status: Status
    priority: Priority
    tags: list[str]
    created_at: datetime
    completed_at: Optional[datetime] = None


# In-memory store, seeded with sample data
_tasks: dict[UUID, Task] = {}


def _seed():
    for title, desc, priority, status, tags in [
        ("Set up CI/CD", "Configure GitHub Actions pipeline",
         Priority.HIGH, Status.DONE, ["devops", "infrastructure"]),
        ("Write API docs", "Document all GraphQL endpoints",
         Priority.MEDIUM, Status.IN_PROGRESS, ["docs"]),
        ("Fix login bug", "Users getting 401 on valid credentials",
         Priority.CRITICAL, Status.TODO, ["bug", "auth"]),
        ("Add dark mode", "Support dark theme in web app",
         Priority.LOW, Status.TODO, ["frontend", "ui"]),
    ]:
        task = Task(
            id=uuid4(), title=title, description=desc,
            status=status, priority=priority, tags=tags,
            created_at=datetime(2026, 1, 15, 9, 0, 0),
            completed_at=datetime(2026, 2, 1, 14, 30, 0) if status == Status.DONE else None,
        )
        _tasks[task.id] = task


_seed()


class TaskManagerAPI:

    @field
    def tasks(
        self,
        status: Optional[Status] = None,
        priority: Optional[Priority] = None,
    ) -> list[Task]:
        """List all tasks, optionally filtered by status or priority."""
        result = list(_tasks.values())
        if status is not None:
            result = [t for t in result if t.status == status]
        if priority is not None:
            result = [t for t in result if t.priority == priority]
        return result

    @field
    def task(self, id: UUID) -> Optional[Task]:
        """Get a single task by ID."""
        return _tasks.get(id)

    @field(mutable=True)
    def create_task(
        self,
        title: str,
        description: str = "",
        priority: Priority = Priority.MEDIUM,
        tags: Optional[list[str]] = None,
    ) -> Task:
        """Create a new task."""
        task = Task(
            id=uuid4(),
            title=title,
            description=description,
            status=Status.TODO,
            priority=priority,
            tags=tags or [],
            created_at=datetime.now(),
        )
        _tasks[task.id] = task
        return task

    @field(mutable=True)
    def update_status(self, id: UUID, status: Status) -> Task:
        """Update a task's status. Automatically sets completed_at when done."""
        task = _tasks[id]
        task.status = status
        if status == Status.DONE:
            task.completed_at = datetime.now()
        elif task.completed_at is not None:
            task.completed_at = None
        return task

    @field(mutable=True)
    def delete_task(self, id: UUID) -> bool:
        """Delete a task by ID. Returns true if the task existed."""
        if id in _tasks:
            del _tasks[id]
            return True
        return False


api = GraphQLAPI(root_type=TaskManagerAPI)
server = GraphQLMCP.from_api(api, allow_mutations=True, graphql_http_kwargs={
    "graphiql_example_query": """\
# List all tasks
{
  tasks {
    id
    title
    status
    priority
    tags
    createdAt
  }
}

# Filter by status
# {
#   tasks(status: TODO) {
#     title
#     priority
#   }
# }

# Create a task
# mutation {
#   createTask(
#     title: "My new task"
#     description: "Something important"
#     priority: HIGH
#     tags: ["example"]
#   ) {
#     id
#     title
#     createdAt
#   }
# }""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8003)

Nested API

Nested tools, @mcp directive, Pydantic models, and async resolvers.

nested_api.py
"""Nested API - demonstrates nested tools, the @mcp directive, Pydantic models, and async resolvers.

Demonstrates:
- Nested query paths that auto-generate MCP tools (category → articles)
- @mcp(hidden=True) to hide arguments from MCP tools
- Pydantic BaseModel types as GraphQL object types
- Async resolvers
- Separate query_type / mutation_type pattern
"""

from typing import Annotated, Optional
from uuid import uuid4

from pydantic import BaseModel

from graphql_api import GraphQLAPI, field
from graphql_mcp import GraphQLMCP, mcp


class Comment(BaseModel):
    id: str
    author: str
    text: str


class Article(BaseModel):
    id: str
    title: str
    body: str
    tags: list[str]
    comments: list[Comment] = []


# In-memory store keyed by category name
_categories: dict[str, list[Article]] = {}


def _seed():
    _categories["python"] = [
        Article(
            id=str(uuid4()), title="Getting Started with FastAPI",
            body="FastAPI is a modern web framework for building APIs with Python.",
            tags=["web", "fastapi"],
            comments=[
                Comment(id=str(uuid4()), author="alice", text="Great intro!"),
                Comment(id=str(uuid4()), author="bob", text="Very helpful."),
            ],
        ),
        Article(
            id=str(uuid4()), title="Async Python Patterns",
            body="Learn how to use asyncio effectively in your projects.",
            tags=["async", "patterns"],
            comments=[
                Comment(id=str(uuid4()), author="charlie", text="Exactly what I needed."),
            ],
        ),
    ]
    _categories["graphql"] = [
        Article(
            id=str(uuid4()), title="Schema-First vs Code-First GraphQL",
            body="Comparing the two main approaches to building GraphQL APIs.",
            tags=["architecture", "patterns"],
            comments=[],
        ),
        Article(
            id=str(uuid4()), title="GraphQL Subscriptions Deep Dive",
            body="Understanding real-time data with GraphQL subscriptions.",
            tags=["real-time", "subscriptions"],
            comments=[
                Comment(id=str(uuid4()), author="diana", text="When would you use SSE instead?"),
            ],
        ),
    ]
    _categories["mcp"] = [
        Article(
            id=str(uuid4()), title="Building MCP Servers",
            body="How to expose your API as MCP tools for AI agents.",
            tags=["ai", "mcp"],
            comments=[
                Comment(id=str(uuid4()), author="eve", text="This is the future."),
            ],
        ),
    ]


_seed()


class Category:
    """A category containing articles. Fields with arguments generate nested MCP tools."""

    def __init__(self, name: str, articles: list[Article]):
        self._name = name
        self._articles = articles

    @field
    async def articles(
        self,
        tag: Optional[str] = None,
        internal_score: Annotated[Optional[int], mcp(hidden=True)] = None,
    ) -> list[Article]:
        """List articles in this category, optionally filtered by tag.

        The internal_score argument is hidden from MCP tools via
        @mcp(hidden: true) but remains accessible through the GraphQL
        API directly.
        """
        result = self._articles
        if tag is not None:
            result = [a for a in result if tag in a.tags]
        return result

    @field
    def name(self) -> str:
        """The category name."""
        return self._name

    @field
    def article_count(self) -> int:
        """Number of articles in this category."""
        return len(self._articles)


class Query:

    @field
    def categories(self) -> list[str]:
        """List all category names."""
        return list(_categories.keys())

    @field
    def category(self, name: str) -> Optional[Category]:
        """Get a category by name."""
        articles = _categories.get(name)
        if articles is None:
            return None
        return Category(name, articles)


class Mutation:

    @field(mutable=True)
    def add_article(
        self,
        category: str,
        title: str,
        body: str,
        tags: Optional[list[str]] = None,
    ) -> Article:
        """Add an article to a category. Creates the category if it doesn't exist."""
        article = Article(
            id=str(uuid4()),
            title=title,
            body=body,
            tags=tags or [],
        )
        if category not in _categories:
            _categories[category] = []
        _categories[category].append(article)
        return article


api = GraphQLAPI(
    query_type=Query,
    mutation_type=Mutation,
    directives=[mcp],
)
server = GraphQLMCP.from_api(api, allow_mutations=True, graphql_http_kwargs={
    "graphiql_example_query": """\
# Browse the knowledge base
{
  categories

  category(name: "python") {
    name
    articleCount
    articles {
      title
      tags
      comments {
        author
        text
      }
    }
  }
}

# Filter articles by tag
# {
#   category(name: "python") {
#     articles(tag: "async") {
#       title
#       body
#     }
#   }
# }

# Add an article
# mutation {
#   addArticle(
#     category: "python"
#     title: "My New Article"
#     body: "Article content here"
#     tags: ["tutorial"]
#   ) {
#     id
#     title
#   }
# }""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8005)

Remote API

Wraps a public GraphQL API (Countries) as MCP tools via from_remote_url().

remote_api.py
"""Remote API - wraps a public GraphQL API as MCP tools.

Demonstrates:
- GraphQLMCP.from_remote_url() to introspect and wrap any GraphQL endpoint
- Auto-generated MCP tools from a remote schema
- Read-only access (allow_mutations=False)

Uses the public Countries GraphQL API (https://countries.trevorblades.com).
"""

from graphql_mcp.server import GraphQLMCP

server = GraphQLMCP.from_remote_url(
    "https://countries.trevorblades.com/graphql",
    allow_mutations=False,
    graphql_http_kwargs={
        "graphiql_example_query": """\
{
  countries(filter: { continent: { eq: "EU" } }) {
    name
    capital
    emoji
    languages {
      name
    }
  }
}""",
    },
)
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8004)

graphql-api

Expose a graphql-api app as MCP tools, customized with the @mcp directive.

library_graphql_api.py
"""graphql-api → MCP tools, customized with the @mcp directive.

Exposes a small "users" API and uses @mcp to rename a tool
(get_user_by_id → fetch_user), rename and describe an argument (user_id → id),
hide an argument (debug_token), and hide a field (internal_metrics).

With graphql-api you apply @mcp directly: as a decorator on fields and inside
Annotated[...] on arguments (and pass directives=[mcp] to GraphQLAPI).
"""

from typing import Annotated, Optional

from graphql_api import GraphQLAPI, field

from graphql_mcp import GraphQLMCP, mcp

# Tiny in-memory "database".
_USERS = {"1": "Alice", "2": "Bob"}


class UsersAPI:

    @field
    def list_users(self) -> list[str]:
        """List all user names."""
        return list(_USERS.values())

    @field
    @mcp(name="fetch_user", description="Fetch a user by ID.")
    def get_user_by_id(
        self,
        user_id: Annotated[str, mcp(name="id", description="The user's unique ID.")],
        # Hidden from MCP, but still usable via the GraphQL API directly.
        debug_token: Annotated[str, mcp(hidden=True)] = "",
    ) -> Optional[str]:
        return _USERS.get(user_id)

    @field
    @mcp(hidden=True)
    def internal_metrics(self) -> str:
        """Internal diagnostics — hidden from MCP via @mcp(hidden=True)."""
        return "cpu=0.3"


api = GraphQLAPI(root_type=UsersAPI, directives=[mcp])
server = GraphQLMCP.from_api(api, graphql_http_kwargs={
    "graphiql_example_query": """\
{
  listUsers

  # Exposed to MCP as the "fetch_user" tool, with arg "id".
  getUserById(userId: "1")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8010)

graphql-core

Expose a graphql-core schema as MCP tools, customized with the @mcp directive.

library_graphql_core.py
"""graphql-core → MCP tools, customized with the @mcp directive.

Exposes a small "users" API and uses @mcp to rename a tool
(getUserById → fetch_user), rename and describe an argument (userId → id),
hide an argument (debugToken), and hide a field (internalMetrics).

With graphql-core you declare and apply @mcp inline in your SDL.
"""

from graphql import build_schema

from graphql_mcp.server import GraphQLMCP

# Declare the @mcp directive, then use it inline in the schema.
SDL = """
directive @mcp(
    name: String
    description: String
    hidden: Boolean
) on FIELD_DEFINITION | ARGUMENT_DEFINITION

type Query {
    "List all user names."
    listUsers: [String!]!

    getUserById(
        userId: ID! @mcp(name: "id", description: "The user's unique ID.")
        debugToken: String = "" @mcp(hidden: true)
    ): String @mcp(name: "fetch_user", description: "Fetch a user by ID.")

    internalMetrics: String @mcp(hidden: true)
}
"""

_USERS = {"1": "Alice", "2": "Bob"}

schema = build_schema(SDL)

# Attach resolvers to the graphql-core schema.
schema.query_type.fields["listUsers"].resolve = (
    lambda root, info: list(_USERS.values())
)
schema.query_type.fields["getUserById"].resolve = (
    lambda root, info, userId, debugToken="": _USERS.get(userId)
)
schema.query_type.fields["internalMetrics"].resolve = (
    lambda root, info: "cpu=0.3"
)

server = GraphQLMCP(schema=schema, name="Users (graphql-core)", graphql_http_kwargs={
    "graphiql_example_query": """\
{
  listUsers

  # Exposed to MCP as the "fetch_user" tool, with arg "id".
  getUserById(userId: "1")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8011)

Ariadne

Expose a schema-first Ariadne API as MCP tools, customized with the @mcp directive.

library_ariadne.py
"""Ariadne → MCP tools, customized with the @mcp directive.

Exposes a small "users" API and uses @mcp to rename a tool
(getUserById → fetch_user), rename and describe an argument (userId → id),
hide an argument (debugToken), and hide a field (internalMetrics).

With schema-first Ariadne you declare and apply @mcp inline in your SDL type_defs.
"""

from ariadne import QueryType, make_executable_schema

from graphql_mcp.server import GraphQLMCP

type_defs = """
    directive @mcp(
        name: String
        description: String
        hidden: Boolean
    ) on FIELD_DEFINITION | ARGUMENT_DEFINITION

    type Query {
        "List all user names."
        listUsers: [String!]!

        getUserById(
            userId: ID! @mcp(name: "id", description: "The user's unique ID.")
            debugToken: String = "" @mcp(hidden: true)
        ): String @mcp(name: "fetch_user", description: "Fetch a user by ID.")

        internalMetrics: String @mcp(hidden: true)
    }
"""

_USERS = {"1": "Alice", "2": "Bob"}

query = QueryType()


@query.field("listUsers")
def resolve_list_users(_, info):
    return list(_USERS.values())


@query.field("getUserById")
def resolve_get_user_by_id(_, info, userId, debugToken=""):
    return _USERS.get(userId)


@query.field("internalMetrics")
def resolve_internal_metrics(_, info):
    return "cpu=0.3"


schema = make_executable_schema(type_defs, query)
server = GraphQLMCP(schema=schema, name="Users (Ariadne)", graphql_http_kwargs={
    "graphiql_example_query": """\
{
  listUsers

  # Exposed to MCP as the "fetch_user" tool, with arg "id".
  getUserById(userId: "1")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8012)

Strawberry

Expose a Strawberry API as MCP tools, customized with the @mcp directive.

library_strawberry.py
"""Strawberry → MCP tools, customized with the @mcp directive.

Exposes a small "users" API and uses @mcp to rename a tool
(get_user_by_id → fetch_user), rename and describe an argument (user_id → id),
hide an argument (debug_token), and hide a field (internal_metrics).

Note: with Strawberry you declare @mcp as a `@strawberry.schema_directive` and
round-trip the schema through SDL (`build_schema(str(schema))`) so the directive
reaches the graphql-core AST, then copy resolvers across. Other approaches (e.g.
`apply_mcp()`) are in the Strawberry & Graphene guide:
https://graphql-mcp.com/strawberry-graphene
"""

from typing import Annotated, Optional

import strawberry
from strawberry.schema_directive import Location

from graphql import GraphQLSchema, build_schema

from graphql_mcp.server import GraphQLMCP


@strawberry.schema_directive(
    locations=[Location.FIELD_DEFINITION, Location.ARGUMENT_DEFINITION]
)
class Mcp:
    name: str = ""
    description: str = ""
    hidden: bool = False


_USERS = {"1": "Alice", "2": "Bob"}


@strawberry.type
class Query:

    @strawberry.field
    def list_users(self) -> list[str]:
        """List all user names."""
        return list(_USERS.values())

    @strawberry.field(
        directives=[Mcp(name="fetch_user", description="Fetch a user by ID.")]
    )
    def get_user_by_id(
        self,
        user_id: Annotated[
            str,
            strawberry.argument(
                directives=[Mcp(name="id", description="The user's unique ID.")]
            ),
        ],
        debug_token: Annotated[
            str, strawberry.argument(directives=[Mcp(hidden=True)])
        ] = "",
    ) -> Optional[str]:
        return _USERS.get(user_id)

    @strawberry.field(directives=[Mcp(hidden=True)])
    def internal_metrics(self) -> str:
        return "cpu=0.3"


def to_core_schema(strawberry_schema: strawberry.Schema) -> GraphQLSchema:
    """Round-trip a Strawberry schema to graphql-core, preserving @mcp.

    `str(strawberry_schema)` emits the directive definitions and applications,
    so rebuilding from that SDL keeps them on the AST. Resolvers don't survive
    SDL printing, so copy them back across by field name.
    """
    rebuilt = build_schema(str(strawberry_schema))
    for type_name in ("query_type", "mutation_type"):
        src = getattr(strawberry_schema._schema, type_name)
        dst = getattr(rebuilt, type_name)
        if src is None or dst is None:
            continue
        for field_name, src_field in src.fields.items():
            if field_name in dst.fields:
                dst.fields[field_name].resolve = src_field.resolve
    return rebuilt


schema = to_core_schema(strawberry.Schema(query=Query))
server = GraphQLMCP(schema=schema, name="Users (Strawberry)", graphql_http_kwargs={
    "graphiql_example_query": """\
{
  listUsers

  # Exposed to MCP as the "fetch_user" tool, with arg "id".
  getUserById(userId: "1")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8013)

Graphene

Expose a Graphene API as MCP tools, customized with the @mcp directive.

library_graphene.py
"""Graphene → MCP tools, customized with the @mcp directive.

Exposes a small "users" API and uses @mcp to rename a tool
(get_user_by_id → fetch_user), rename and describe an argument (user_id → id),
hide an argument (debug_token), and hide a field (internal_metrics).

Note: with Graphene you apply @mcp via `apply_mcp()`, which attaches the config
to the graphql-core schema directly (keyed by "Type.field" / "Type.field.arg").
More in the Strawberry & Graphene guide: https://graphql-mcp.com/strawberry-graphene
"""

import graphene

from graphql_mcp import GraphQLMCP, apply_mcp

_USERS = {"1": "Alice", "2": "Bob"}


class Query(graphene.ObjectType):
    list_users = graphene.List(
        graphene.NonNull(graphene.String), required=True,
        description="List all user names.",
    )
    get_user_by_id = graphene.String(
        user_id=graphene.ID(required=True),
        debug_token=graphene.String(default_value=""),
    )
    internal_metrics = graphene.String()

    def resolve_list_users(root, info):
        return list(_USERS.values())

    def resolve_get_user_by_id(root, info, user_id, debug_token=""):
        return _USERS.get(user_id)

    def resolve_internal_metrics(root, info):
        return "cpu=0.3"


# Graphene exposes Python snake_case names as camelCase in the schema, so the
# apply_mcp paths use the GraphQL (camelCase) names.
schema = graphene.Schema(query=Query).graphql_schema
apply_mcp(
    schema,
    fields={
        "Query.getUserById": {"name": "fetch_user", "description": "Fetch a user by ID."},
        "Query.internalMetrics": {"hidden": True},
    },
    args={
        "Query.getUserById.userId": {"name": "id", "description": "The user's unique ID."},
        "Query.getUserById.debugToken": {"hidden": True},
    },
)

server = GraphQLMCP(schema=schema, name="Users (Graphene)", graphql_http_kwargs={
    "graphiql_example_query": """\
{
  listUsers

  # Exposed to MCP as the "fetch_user" tool, with arg "id".
  getUserById(userId: "1")
}""",
})
app = server.http_app(transport="streamable-http", stateless_http=True)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8014)