Large Language ModelsGenerate imagesGenerate videos
MCP Server Examples in Python and TypeScript (GitHub Code) That Run on the Current SDKs
Copy-ready MCP server examples in Python and TypeScript, built on the official SDK repositories on GitHub. Each sample runs over stdio or Streamable HTTP, and the last two wrap an image and video API so an assistant can generate media from a chat window.
An MCP server is a small program that hands an AI assistant a list of tools it can call, and the quickest way to see how one works is to read a few that already run. This article collects MCP server examples in Python and TypeScript, written against the official SDK repositories on GitHub, so you can paste a file, start it, and watch an assistant use it within minutes.
Everything below follows the SDK documentation as of October 2026, which matters because both SDKs recently reached a second major version. In Python, FastMCP became MCPServer. In TypeScript, the server code moved into its own @modelcontextprotocol/server package. The first half of the article builds a plain server in each language. The second half adds tools that generate images and video, which is where MCP stops being a demo and starts saving real work.
What an MCP Server Does
The Model Context Protocol (MCP) is an open standard that lets an AI client, such as a chat app, an IDE, or an agent, call code you wrote. The client opens a connection, asks your server what it offers, and lets the model decide when to use each item. Messages travel as JSON-RPC, and your server never calls the model itself. It waits to be asked, runs the function, and returns a result.
Tools, resources, and prompts
Every server is built from three kinds of building block:
Tools are functions the model can call, like add or generate_image. They can have side effects, so they are the part to design with care.
Resources are read-only data addressed by a URI, such as greeting://alice or a file path. The client reads them to give the model context.
Prompts are reusable message templates that a person picks from a menu, for example a code review request.
💡 Tip: Ship tools first. Most servers on GitHub expose only tools, and a model picks the right one more reliably from a short list of clearly named tools than from a long list of vague ones.
Which SDK line to install
Both official SDKs now have a current line and a maintenance line. Pick the right one before you copy code, because the imports differ.
Python
TypeScript
Current (v2)
pip install "mcp[cli]", class MCPServer
npm install @modelcontextprotocol/server
Maintenance (v1.x)
pip install "mcp[cli]<2", class FastMCP
npm install @modelcontextprotocol/sdk zod
GitHub repository
modelcontextprotocol/python-sdk
modelcontextprotocol/typescript-sdk
The Python v1 line only receives security fixes now, so new projects should start on v2, and the Python samples below do. If you maintain an older Python server, migrating means changing from mcp.server.fastmcp import FastMCP to from mcp.server.mcpserver import MCPServer and renaming the constructor call. The decorators stay the same.
For TypeScript, the full samples use the v1.x package that most existing servers import today. A short v2 version of the same server follows, so you can see exactly what changes.
Python Example With MCPServer
Python has the shortest path from nothing to a working server, because type hints and docstrings become the tool schema. There is no JSON Schema to write by hand.
The server file
Install with uv add "mcp[cli]" (or pip install "mcp[cli]"), then save this as server.py:
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@mcp.prompt()
def review_code(code: str) -> str:
"""Ask for a code review that lists bugs first."""
return f"Please review this code and list bugs first:\n\n{code}"
if __name__ == "__main__":
mcp.run(transport="stdio")
The add function becomes a tool whose input schema is generated from a: int, b: int. The docstring becomes the description the model reads when it decides whether to call it. greeting is a resource template: a client that reads greeting://Ada gets back Hello, Ada!.
Run and inspect it
uv run mcp dev server.py # opens the MCP Inspector in your browser
uv run mcp run server.py # plain stdio, for a client to launch
uv run mcp run server.py --transport streamable-http # HTTP instead
Use the Inspector first. It lists every tool, gives you a form for the arguments, and shows the raw JSON-RPC traffic, which is the fastest way to catch a bad schema. Once it works, register the server with a client. In Claude Code that is one line:
claude mcp add demo -- uv run mcp run server.py
TypeScript Example With Zod
TypeScript asks for a little more ceremony, because you describe inputs with Zod instead of type hints. In exchange you get validation at runtime and typed arguments in your handler.
The server file on v1.x
Install the package with npm install @modelcontextprotocol/sdk zod and set "type": "module" in package.json. Write the server as a function so both transports can reuse it. Save this as src/build-server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function buildServer(): McpServer {
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{
title: "Add numbers",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
return server;
}
Then a three-line entry point for stdio, in src/stdio.ts:
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { buildServer } from "./build-server.js";
await buildServer().connect(new StdioServerTransport());
Compile with tsc, then check it with npx @modelcontextprotocol/inspector node dist/stdio.js. Resources and prompts use the same shape through registerResource and registerPrompt. The v1.x repository also ships src/examples/server/simpleStreamableHttp.ts, a feature-rich sample with tools, resources, prompts, logging, and optional OAuth, which is worth reading once your server grows past a toy.
The same server on v2
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{
description: "Add two numbers",
inputSchema: z.object({ a: z.number(), b: z.number() }),
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
await server.connect(new StdioServerTransport());
Three things change: the package name, the /stdio subpath import, and the schema, which becomes a full z.object(...) from zod/v4 instead of a plain object of fields. The handler body is identical. Serving over HTTP on v2 goes through small adapter packages such as @modelcontextprotocol/express, so read that package's README before you port an HTTP server.
Stdio or Streamable HTTP
The transport is the only decision that changes how you deploy. The tool code stays identical.
stdio
Streamable HTTP
Who starts the server
The client launches it as a child process
You run it, clients connect by URL
Best for
Local tools, IDEs, desktop apps
Shared or remote servers, teams
Authentication
Inherits your user and environment
You add it (tokens or OAuth)
Logging
stderr only
Normal output is fine
Scaling
One process per client
Ordinary web scaling
Local process over stdio
A stdio server is the right default for anything that touches your own machine, such as files, a local database, or a script. The client spawns it, talks through its stdin and stdout, and stops it when the session ends. There is no port to secure.
Remote server over HTTP
Streamable HTTP lets one running server answer many clients. This stateless Express version builds a fresh server for every request, which avoids shared session state. Save it as src/http.ts:
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./build-server.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, "127.0.0.1");
Register it with claude mcp add --transport http demo http://127.0.0.1:3000/mcp. In Python the equivalent is one line: mcp.run(transport="streamable-http", host="127.0.0.1", port=9000). Bind to localhost unless something in front of the server handles authentication, because an open MCP port is an open door to every tool you registered.
Example: Tools That Generate Images
Tools get more interesting when the result is not a number. Image and video generation make good teaching cases because they are slow, asynchronous, and return a URL instead of text. The samples below call the PicassoIA API, which follows a Replicate style: create a prediction, poll it, read the output.
Base URL and auth:https://api.picassoia.com/v1 with the header Authorization: Bearer pia_sk_...
Create:POST /models/{owner}/{name}/predictions with the body {"input": {...}}
Poll:GET /predictions/{id} until the status is succeeded, failed, or canceled
Timing: the create response includes eta.next_poll_in_seconds, a polling interval worth respecting
Python tool with polling
import asyncio
import os
import httpx
from mcp.server.mcpserver import MCPServer
API = "https://api.picassoia.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PICASSOIA_API_TOKEN']}"}
mcp = MCPServer("picassoia-media")
slots = asyncio.Semaphore(5) # the account allows 5 concurrent predictions
async def run_prediction(model: str, payload: dict, timeout_s: int = 600) -> list[str]:
async with slots, httpx.AsyncClient(headers=HEADERS, timeout=30) as http:
created = await http.post(
f"{API}/models/{model}/predictions", json={"input": payload}
)
created.raise_for_status()
prediction = created.json()
waited = 0
while prediction["status"] in ("starting", "processing"):
if waited >= timeout_s:
raise TimeoutError(f"Prediction {prediction['id']} is still running")
delay = (prediction.get("eta") or {}).get("next_poll_in_seconds", 3)
await asyncio.sleep(delay)
waited += delay
polled = await http.get(f"{API}/predictions/{prediction['id']}")
polled.raise_for_status()
prediction = polled.json()
if prediction["status"] != "succeeded":
raise RuntimeError(f"Prediction {prediction['status']}: {prediction.get('error')}")
output = prediction["output"]
return output if isinstance(output, list) else [output]
@mcp.tool()
async def generate_image(prompt: str, aspect_ratio: str = "16:9") -> str:
"""Generate one image from a text prompt and return its URL."""
urls = await run_prediction(
"picassoia/picassoia-image",
{"prompt": prompt, "aspect_ratio": aspect_ratio, "num_outputs": 1},
)
return urls[0]
if __name__ == "__main__":
mcp.run(transport="stdio")
Two details matter here. The loop uses asyncio.sleep, so the server stays responsive to other requests while a job runs, and it follows the interval the API suggests instead of hammering it. The semaphore keeps you under the account's concurrency limit. The PicassoIA Image model accepts prompt, aspect_ratio, seed, num_outputs (1 or 2), output_format, and output_quality. For edits, point the same helper at PicassoIA Image Editor Pro.
TypeScript tool for video
The same pattern works in TypeScript. This version adds a video tool on top of the buildServer function from earlier:
const API = "https://api.picassoia.com/v1";
const headers = {
Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
"Content-Type": "application/json",
};
async function runPrediction(model: string, input: Record<string, unknown>) {
const created = await fetch(`${API}/models/${model}/predictions`, {
method: "POST",
headers,
body: JSON.stringify({ input }),
});
if (!created.ok) throw new Error(`Create failed: ${created.status}`);
let prediction = await created.json();
while (["starting", "processing"].includes(prediction.status)) {
const delay = prediction.eta?.next_poll_in_seconds ?? 5;
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
const polled = await fetch(`${API}/predictions/${prediction.id}`, { headers });
prediction = await polled.json();
}
if (prediction.status !== "succeeded") {
throw new Error(`Prediction ${prediction.status}`);
}
return [prediction.output].flat() as string[];
}
server.registerTool(
"generate_video",
{
title: "Generate video",
description: "Make a short clip with synchronized audio from a text prompt",
inputSchema: {
prompt: z.string().max(4000),
duration: z.union([z.literal(5), z.literal(10)]).default(5),
resolution: z.enum(["480p", "720p"]).default("720p"),
},
},
async ({ prompt, duration, resolution }) => {
const [url] = await runPrediction("picassoia/seedance-2.5-lite", {
prompt,
duration,
resolution,
});
return { content: [{ type: "text", text: url }] };
}
);
Video is slower. The Seedance 2.5 Lite model page lists example runs of roughly 100 to 190 seconds, which is long enough that some clients give up on a single tool call. A safer design splits the work in two: start_video returns the prediction id immediately, and check_video takes that id and returns either the status or the final URL.
That is how the PicassoIA connector for claude.ai behaves. Its generate tools return a predict_id and a suggested wait, and get_generation is called until the job succeeds or fails. The same model also takes an optional image as the first frame, a seed, an aspect_ratio, and a save_audio flag for silent clips.
💡 Tip: Keep the tool result small. Return the URL and a one-line summary, not the file. The assistant only needs a link to show or pass along.
How to Use PicassoIA With MCP
There are two ways to reach PicassoIA from an assistant: the ready-made connector, or your own server wrapped around the API, as in the samples above.
Step by step setup
Pick the route. For a chat client, add the PicassoIA connector in your claude.ai settings. It exposes generate_image, edit_image, generate_video_picassoia, generate_video_seedance, get_generation, cancel_generation, list_models, get_account, and list_generations. For your own agent code, use the API route.
Create an API token. Sign in to the PicassoIA API page and create one. It starts with pia_sk_, and an account holds at most two. Check the pricing page to see which plan includes API access before you build on it.
Store it in the environment. Run export PICASSOIA_API_TOKEN=pia_sk_... in your shell. Keep it out of source files and out of any client config you commit.
Choose a model from the table below.
Wrap and test. Paste a tool from the samples, run it in the Inspector with a simple prompt, then register it with your client.
Text or image to video with synchronized audio, 5 or 10 seconds
Which model decides when to call your tools? Any tool-calling LLM can. PicassoIA's large language model catalogue lists Claude Sonnet 5, GPT 5.6 Sol, Kimi K2.6, and Gemini 3.5 Flash, among many others. Try more than one against the same server and compare how reliably each picks the right tool.
Limits worth knowing
5 concurrent predictions per account, shared across tokens and MCP connections
10 MB maximum request body
4,000 characters maximum per prompt
3 hours before a prediction times out
2 API tokens per account
Mistakes That Break MCP Servers
Most failed first runs trace back to one of three problems. Each one is easy to avoid once you know to look for it.
Logging to stdout
With stdio, standard output is the protocol channel. A stray print() or console.log() corrupts the JSON-RPC stream, and the client reports a parse error or a server that never connected. Send logs to standard error instead:
In TypeScript, use console.error("polling prediction").
Blocking calls and vague names
A time.sleep(30) inside an async tool freezes every other request on the same server. Use await asyncio.sleep in Python and an awaited timer in TypeScript, as the samples do. Names matter just as much: a tool called do_task gives the model nothing to choose with, while generate_image with a clear docstring tells it exactly when to reach for the tool.
Secrets and large payloads
Read tokens from environment variables, never from source files, and never echo them in a tool result. For output, return URLs instead of base64. A single base64 image can run to megabytes and floods the model's context window, and the PicassoIA API caps request bodies at 10 MB anyway.
Try It on PicassoIA Today
Copy server.py or the TypeScript pair, run it in the Inspector, and you have a working MCP server in under ten minutes. Then give it something visual to do. The official reference servers on GitHub are a good place to read more patterns, and both SDK repositories include an examples folder.
Open PicassoIA Image and write a prompt of your own, edit a shot with Image Editor Pro, or animate a frame with Seedance 2.5 Lite. Whatever you build, the loop is the same: describe, submit, poll, review.
Try creating your own images with Picasso IA, and when a prompt works, wrap it as a tool so your assistant can repeat it on demand.