MCP Overview
Model Context Protocol (MCP) is an open protocol that lets AI agents interact with external servers. This page explains MCP's core concepts through lynq code examples.
How MCP Works
AI Agent (Claude, Cursor, etc.) ←──JSON-RPC──→ MCP Server (your code)
↕
External API (optional)The agent connects to your server. Your server exposes tools, resources, and tasks. The agent calls tools, reads resources, and runs tasks as needed to accomplish the user's goal.
Your MCP server can optionally call external APIs behind the scenes. The agent doesn't see the API directly -- it only sees the tools and resources you expose.
Tools
A tool is a function the agent can call. You define the name, input schema, and handler:
import { createMCPServer } from "@lynq/lynq";
import { z } from "zod";
const server = createMCPServer({ name: "my-server", version: "1.0.0" });
server.tool(
"get_weather",
{
description: "Get weather for a city",
input: z.object({ city: z.string() }),
},
async (args, c) => c.text(`${args.city}: 22°C, Sunny`),
);When the user asks "What's the weather in Tokyo?", the agent:
- Sees
get_weatherin the tool list - Calls
get_weather({ city: "Tokyo" }) - Gets back
"Tokyo: 22°C, Sunny" - Uses the result to answer the user
Resources
A resource is data the agent can read, identified by a URI:
server.resource(
"config://settings",
{ name: "App Settings", mimeType: "application/json" },
async () => ({ text: '{"theme":"dark","lang":"en"}' }),
);The agent can list available resources and read them by URI. Unlike tools, resources are read-only -- they don't take input arguments.
Tasks
A task is a long-running operation with progress reporting. Like tools, but the agent can track progress and cancel mid-execution:
server.task(
"analyze_data",
{
description: "Run data analysis",
input: z.object({ query: z.string() }),
},
async (args, c) => {
c.task.progress(0, "Starting...");
const data = await fetchData(args.query);
c.task.progress(50, "Processing...");
const result = analyzeData(data);
c.task.progress(100, "Done");
return c.text(JSON.stringify(result));
},
);Tasks are experimental in the MCP SDK. lynq's
server.task()interface is stable, but the underlying SDK wiring may change.
External APIs
An MCP server often wraps external APIs. The agent calls your tool; your tool calls the API:
server.tool(
"search_github",
{
description: "Search GitHub repositories",
input: z.object({ query: z.string() }),
},
async (args, c) => {
const res = await fetch(
`https://api.github.com/search/repositories?q=${args.query}`
);
const data = await res.json();
return c.json(data.items.map((r: any) => r.full_name));
},
);The agent sees search_github as a tool. It doesn't know or care that GitHub's API is behind it. This is the typical MCP pattern: your server is a bridge between the agent and any external service.
Transports
MCP uses two transport modes to connect agents and servers:
Stdio -- The agent spawns your server as a child process. Communication happens over stdin/stdout. Used by Claude Desktop and Claude Code for local servers.
await server.stdio();HTTP -- The server runs as a web service. The agent connects over HTTP. Used for remote/shared servers.
const handler = server.http();
// Mount on Hono, Express, Deno, etc.See Transports for details.
What lynq Adds
The official MCP SDK gives you the protocol layer. lynq adds:
Middleware -- Attach logic to tool registration and invocation. Like Hono's middleware, but for MCP tools.
server.use(logger); // global
server.tool("search", rateLimit(10), config, handler); // per-toolSession-scoped visibility -- Tools can appear and disappear per session. The MCP protocol supports this via notifications/tools/list_changed, but wiring it by hand is tedious. lynq handles it automatically.
// Hidden until c.session.authorize("guard")
server.tool("admin_panel", guard(), config, handler);Elicitation -- Pause tool execution and ask the user for input. Two modes: form for structured data, URL for external redirects (OAuth, payments).
// Form mode -- structured data with Zod
const result = await c.elicit.form("Set preferences", z.object({
theme: z.enum(["light", "dark"]),
}));
// URL mode -- external redirect with callback
await c.elicit.url("Complete payment", paymentUrl, {
waitForCompletion: true,
});See Elicitation for details.
Sampling -- Request LLM inference from the client. Your tool provides the prompt; the client decides which model to use.
const answer = await c.sample("Summarize this text", { maxTokens: 100 });
// For full control: c.sample.raw({ messages: [...], maxTokens: 256 })See Sampling for details.
Tasks -- Long-running operations with progress reporting and cancellation:
server.task("analyze", config, async (args, c) => {
c.task.progress(50, "Halfway...");
return c.text("Done");
});See Tasks for details.
Under the hood
MCP is a bidirectional protocol. The server can push notifications to the client at any time -- for example, notifications/tools/list_changed tells the client to re-fetch the tool list. lynq uses this to make tools appear and disappear based on session state. You declare the rules as middleware; lynq sends the notifications.
Next Steps
- Quick Start -- build and run your first server