From typescript-experts
Provides TypeScript patterns for FastMCP MCP servers: basic setup, tools with Zod schemas, logging, progress reporting, streaming output, multiple content types, and image handling.
npx claudepluginhub jpoutrin/product-forge --plugin typescript-expertsThis skill uses the workspace's default tool permissions.
FastMCP is a TypeScript framework for building Model Context Protocol (MCP) servers. This skill provides patterns for tools, resources, prompts, authentication, and transport configuration.
Builds, extends, and debugs FastMCP v3 Python MCP servers including tool/resource creation, providers, transforms (CodeMode, Tool Search), auth (MultiAuth, PropelAuth), client SDK, nginx deployment, and testing.
Provides patterns, architecture diagrams, and decision trees for building, testing, and deploying Model Context Protocol (MCP) servers in Python and TypeScript with tools, resources, prompts, and transports like stdio, SSE, streamable HTTP.
Builds MCP servers with Node/TypeScript SDK: register tools/resources/prompts, Zod validation, stdio/Streamable HTTP transports, debugging.
Share bugs, ideas, or general feedback.
FastMCP is a TypeScript framework for building Model Context Protocol (MCP) servers. This skill provides patterns for tools, resources, prompts, authentication, and transport configuration.
npm install fastmcp
import { FastMCP } from "fastmcp";
import { z } from "zod";
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
});
server.start({ transportType: "stdio" });
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args) => {
return String(args.a + args.b);
},
});
server.addTool({
name: "sayHello",
description: "Say hello",
execute: async () => {
return "Hello, world!";
},
});
server.addTool({
name: "download",
parameters: z.object({ url: z.string() }),
execute: async (args, { log }) => {
log.info("Downloading file...", { url: args.url });
// ... process ...
log.info("Downloaded file");
return "done";
},
});
server.addTool({
name: "download",
execute: async (args, { reportProgress }) => {
await reportProgress({ progress: 0, total: 100 });
// ... process ...
await reportProgress({ progress: 100, total: 100 });
return "done";
},
});
server.addTool({
name: "generateText",
parameters: z.object({ prompt: z.string() }),
annotations: {
streamingHint: true,
readOnlyHint: true,
},
execute: async (args, { streamContent }) => {
await streamContent({ type: "text", text: "Starting...\n" });
const words = "The quick brown fox".split(" ");
for (const word of words) {
await streamContent({ type: "text", text: word + " " });
await new Promise(resolve => setTimeout(resolve, 300));
}
return;
},
});
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({ url: z.string() }),
execute: async (args) => {
return {
content: [
{ type: "text", text: "First message" },
{ type: "text", text: "Second message" },
],
};
},
});
import { imageContent } from "fastmcp";
server.addTool({
name: "getImage",
execute: async (args) => {
return imageContent({
url: "https://example.com/image.png",
});
// Or: path: "/path/to/image.png"
// Or: buffer: Buffer.from(...)
},
});
import { audioContent } from "fastmcp";
server.addTool({
name: "getAudio",
execute: async (args) => {
return audioContent({
url: "https://example.com/audio.mp3",
});
},
});
import { UserError } from "fastmcp";
server.addTool({
name: "download",
execute: async (args) => {
if (args.url.startsWith("https://example.com")) {
throw new UserError("This URL is not allowed");
}
return "done";
},
});
server.addTool({
name: "fetch-content",
description: "Fetch content from a URL",
parameters: z.object({ url: z.string() }),
annotations: {
title: "Web Content Fetcher",
readOnlyHint: true,
openWorldHint: true,
},
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
server.addTool({
name: "admin-tool",
description: "An admin-only tool",
canAccess: (auth) => auth?.role === "admin",
execute: async () => "Welcome, admin!",
});
import { type } from "arktype";
server.addTool({
name: "fetch-arktype",
parameters: type({ url: "string" }),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
import * as v from "valibot";
server.addTool({
name: "fetch-valibot",
parameters: v.object({ url: v.string() }),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: await readLogFile(),
};
},
});
server.addResource({
uri: "file:///logs/app.log",
async load() {
return {
blob: 'base64-encoded-data'
};
},
});
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
},
],
async load({ name }) {
return {
text: `Log content for ${name}`,
};
},
});
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
complete: async (value) => {
if (value === "Example") {
return { values: ["Example Log"] };
}
return { values: [] };
},
},
],
async load({ name }) {
return {
text: `Log content for ${name}`,
};
},
});
server.addTool({
name: "get_user_data",
parameters: z.object({ userId: z.string() }),
execute: async (args) => {
return {
content: [
{
type: "resource",
resource: await server.embedded(`user://profile/${args.userId}`),
},
],
};
},
});
server.addPrompt({
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true,
},
],
load: async (args) => {
return "Generate a concise commit message:\n\n" + args.changes;
},
});
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return "Write a poem about " + name + "!";
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
complete: async (value) => {
if (value === "Germ") {
return { values: ["Germany"] };
}
return { values: [] };
},
},
],
});
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return "Write a poem about " + name + "!";
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
enum: ["Germany", "France", "Italy"],
},
],
});
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
authenticate: (request) => {
const apiKey = request.headers["x-api-key"];
if (apiKey !== "expected-key") {
throw new Response(null, {
status: 401,
statusText: "Unauthorized",
});
}
return { id: 1 };
},
});
server.addTool({
name: "sayHello",
execute: async (args, { session }) => {
return "Hello, " + session.id + "!";
},
});
const server = new FastMCP<{ role: "admin" | "user" }>({
authenticate: async (request) => {
const role = request.headers["x-role"] as string;
return { role: role === "admin" ? "admin" : "user" };
},
name: "My Server",
version: "1.0.0",
});
server.addTool({
name: "admin-dashboard",
description: "An admin-only tool",
canAccess: (auth) => auth?.role === "admin",
execute: async () => {
return "Welcome to the admin dashboard!";
},
});
server.addTool({
name: "public-info",
description: "A tool available to everyone",
execute: async () => {
return "This is public information.";
},
});
import { FastMCP } from "fastmcp";
import { GoogleProvider } from "fastmcp/auth";
const authProxy = new GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
baseUrl: "https://your-server.com",
scopes: ["openid", "profile", "email"],
});
const server = new FastMCP({
name: "My Server",
oauth: {
enabled: true,
authorizationServer: authProxy.getAuthorizationServerMetadata(),
proxy: authProxy,
},
});
const sessionCounters = new Map<string, number>();
server.addTool({
name: "increment_counter",
description: "Increment a per-session counter",
parameters: z.object({}),
execute: async (args, context) => {
if (!context.sessionId) {
return "Session ID not available (requires HTTP transport)";
}
const counter = sessionCounters.get(context.sessionId) || 0;
const newCounter = counter + 1;
sessionCounters.set(context.sessionId, newCounter);
return "Counter: " + newCounter;
},
});
server.on("connect", (event) => {
console.log("Client connected:", event.session);
});
server.on("disconnect", (event) => {
console.log("Client disconnected:", event.session);
});
server.on("connect", (event) => {
const session = event.session;
console.log("Initial roots:", session.roots);
session.on("rootsChanged", (event) => {
console.log("Roots changed:", event.roots);
});
session.on("error", (event) => {
console.error("Error:", event.error);
});
});
await session.requestSampling({
messages: [
{
role: "user",
content: {
type: "text",
text: "What files are in the current directory?",
},
},
],
systemPrompt: "You are a helpful file system assistant.",
includeContext: "thisServer",
maxTokens: 100,
});
server.start({
transportType: "httpStream",
httpStream: {
port: 8080,
},
});
server.start({
transportType: "httpStream",
httpStream: {
port: 8080,
endpoint: "/mcp",
},
});
server.start({
transportType: "httpStream",
httpStream: {
port: 8080,
stateless: true,
},
});
server.start({
transportType: "httpStream",
httpStream: {
port: 8080,
},
});
// SSE automatically available at http://localhost:8080/sse
server.start({
transportType: "stdio",
});
import { FastMCP, Logger } from "fastmcp";
class CustomLogger implements Logger {
debug(...args: unknown[]): void {
console.log("[DEBUG]", new Date().toISOString(), ...args);
}
error(...args: unknown[]): void {
console.error("[ERROR]", new Date().toISOString(), ...args);
}
info(...args: unknown[]): void {
console.info("[INFO]", new Date().toISOString(), ...args);
}
log(...args: unknown[]): void {
console.log("[LOG]", new Date().toISOString(), ...args);
}
warn(...args: unknown[]): void {
console.warn("[WARN]", new Date().toISOString(), ...args);
}
}
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
logger: new CustomLogger(),
});
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
health: {
enabled: true,
message: "healthy",
path: "/healthz",
status: 200,
},
});
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
ping: {
enabled: true,
intervalMs: 10000,
logLevel: "debug",
},
});
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
instructions: "This server provides file management tools. Use the list_files tool to see available files.",
});
# Development mode with hot reload
npx fastmcp dev src/server.ts
# HTTP streaming transport
npx fastmcp dev src/server.ts --transport http-stream --port 8080
# Stateless mode
npx fastmcp dev src/server.ts --transport http-stream --port 8080 --stateless true
# Or via environment variable
FASTMCP_STATELESS=true npx fastmcp dev src/server.ts
UserError for user-facing errorsreportProgressstreamContent with streamingHint: truecanAccess for role-based access controllog object for debugging