FOR THE BUILDERS, THE MAKERS & THE SMALL TEAMS WITH BIG IDEAS ✳
Guides · Tutorial

Build an AI Workflow with MCP: A Small, Testable Python Example

Start an MCP workflow with one tool you can inspect and test. This walkthrough builds a release-checklist tool and calls it from a Python client. It uses no model API key, opens no network port, and never deploys anything. The point is to validate the tool boundary before asking an AI application to use it.

MCP workflow: Client request → MCP tool → Typed result. Editorial workflow illustration.
Example verified locally with Python 3.11 and mcp 2.3.0. The in-memory client and both checklist outcomes were tested. The optional Inspector command is documented by the SDK but was not exercised in this walkthrough.

What MCP connects—and what it does not decide

The Model Context Protocol standardizes how AI applications connect to tools and context. Its architecture distinguishes the host application, the clients it manages, and servers that expose capabilities. A server can expose tools, resources, and prompts; the host still needs to decide how those capabilities enter a user workflow.

That distinction matters. Connecting a tool does not make its inputs trustworthy or its results correct. For this example, the tool accepts a list of changed files and a test-status flag. Those are supplied assertions, not independent evidence that a repository passed its tests. The output is a checklist for review, never permission to release.

This is a local integration exercise rather than an autonomous agent. No language model chooses a tool call here. Once the interface is predictable, you can evaluate how an MCP-capable host discovers and uses it.

1. Prepare an isolated example

You need Python 3.11 and uv. Create a new directory and keep the two files below together. Do not name either file mcp.py, because that would shadow the installed package.

mkdir mcp-release-demo
cd mcp-release-demo

The run command below pins mcp==2.3.0 and asks uv for Python 3.11. uv may download the interpreter or dependencies if they are missing. Dependency installation requires network access; the demonstrated client-to-server call itself stays in memory. Use the pinned version for reproduction instead of assuming examples for another SDK generation have the same API.

2. Define a tool with a structured result

Save this as server.py. The SDK registers the function as a tool. A Pydantic model describes the response fields so the client can read structured data instead of parsing prose. This follows the SDK’s documented structured-output approach.

from mcp.server import MCPServer
from pydantic import BaseModel

mcp = MCPServer("Release checklist")


class Checklist(BaseModel):
    changed_file_count: int
    ready_for_human_review: bool
    next_step: str


@mcp.tool()
def release_checklist(changed_files: list[str], tests_passed: bool) -> Checklist:
    """Prepare a release review checklist; never publish or deploy anything."""
    return Checklist(
        changed_file_count=len(changed_files),
        ready_for_human_review=bool(changed_files) and tests_passed,
        next_step="Review the diff and approve separately.",
    )

The tool does not read files, execute tests, write a repository, or publish a release. Keeping it deliberately narrow makes its limitations easy to inspect. ready_for_human_review means only that the two supplied conditions match this example’s rule. It says nothing about code quality, test coverage, or whether a person should approve the diff.

3. Call it through an MCP client

Save this as check_client.py. The Python SDK supports an in-memory client attached to a server object. That lets you test the MCP interface without managing a subprocess, listening port, or host-specific configuration.

import asyncio
from mcp import Client
from server import mcp


async def main():
    async with Client(mcp) as client:
        result = await client.call_tool(
            "release_checklist",
            {"changed_files": ["src/app.py"], "tests_passed": True},
        )
        print(result.structured_content)
        assert result.structured_content["ready_for_human_review"] is True
        blocked = await client.call_tool(
            "release_checklist",
            {"changed_files": ["src/app.py"], "tests_passed": False},
        )
        assert blocked.structured_content["ready_for_human_review"] is False
        print("Both checklist paths passed. No deployment was performed.")


asyncio.run(main())

Run both cases with the pinned package:

uv run --python 3.11 --with "mcp==2.3.0" python check_client.py

The first result should contain changed_file_count: 1 and ready_for_human_review: True. The second call supplies a failed test flag and asserts that readiness is false. A successful run ends with:

Both checklist paths passed. No deployment was performed.

These two assertions verify the example’s decision rule across its passing and blocked paths. They do not establish production reliability or validate a real test runner. If the command fails, confirm the Python and SDK versions and that both files are in the same directory before modifying the code.

4. Inspect the interface before adding autonomy

The official SDK also documents an MCP Inspector workflow. An optional next step is:

uv run --with "mcp[cli]==2.3.0" mcp dev server.py

This optional command was not part of our executed test. Follow the Inspector’s startup instructions and inspect the tool name, description, argument schema, and response. Do not expose a development inspector to the public internet. If you instead connect a particular desktop or editor host, use that host’s current MCP documentation; configuration formats are not interchangeable.

Keep host integration separate from testing the tool. A model might misunderstand a field, omit a necessary step, or provide a plausible-looking value without evidence. Observe those cases explicitly before connecting a repository or external account.

5. Add real evidence one boundary at a time

To evolve this into a useful release-preparation workflow, replace the supplied test flag with an independently recorded test result and bind it to a specific commit. Verify that the diff being reviewed is the same commit the tests covered. Treat missing or stale evidence as a blocked checklist.

  • Collect: read a bounded diff and a trusted test result.
  • Prepare: generate a checklist and identify unresolved issues.
  • Review: let a person inspect the evidence and proposed action.
  • Act: use a separate, explicitly authorized deployment mechanism.

Do not give the checklist server deployment credentials merely because it can describe a release. Add logging that records task IDs and outcomes without storing secrets. Keep retry limits and a clear stopping condition. Our workflow design guide expands on those handoffs, while the AI stack budget guide covers the review and maintenance work around them.

You can download the exact server and client used here. The practical milestone is modest but useful: a tool with an explicit interface, a reproducible call, and a tested condition that prevents this example from presenting failed tests as ready for review.

Sources & further reading

Primary references used for product and technical information. Features and terms can change; check the provider before committing.

No affiliate links are included in this article. Read our disclosure.