MCP सर्वर TypeScript ट्यूटोरियल: SDK उदाहरण और कॉपी करने योग्य टेम्पलेट
npm install से लेकर Claude Code तक, एक काम करने वाला MCP सर्वर TypeScript में। SDK टेम्पलेट कॉपी करें, एक टूल, एक रिसोर्स और एक प्रॉम्प्ट रजिस्टर करें, stdio या Streamable HTTP चुनें, Inspector में टेस्ट करें, फिर इमेज और वीडियो टूल जोड़ें जो क्लाइंट को ब्लॉक किए बिना एक असली API कॉल करते हैं।
आप लगभग चालीस लाइनों में किसी लैंग्वेज मॉडल को अपने कोड से जोड़ सकते हैं। यह MCP सर्वर TypeScript ट्यूटोरियल ठीक यही बनाता है: आधिकारिक SDK पर एक काम करने वाला सर्वर, एक प्रोजेक्ट टेम्पलेट जिसे आप कॉपी कर सकते हैं, और दो ट्रांसपोर्ट जो मायने रखते हैं, स्थानीय क्लाइंट के लिए stdio और रिमोट क्लाइंट के लिए Streamable HTTP। आपके पास एक टूल, एक रिसोर्स और एक प्रॉम्प्ट होगा, जिन्हें Inspector में टेस्ट करके Claude Code में रजिस्टर किया जाएगा। फिर हम उसके ऊपर इमेज और वीडियो टूल जोड़ेंगे, क्योंकि Model Context Protocol सर्वर वहीं से डेमो से आगे बढ़कर असली काम करना शुरू करता है। हर स्निपेट @modelcontextprotocol/sdk पैकेज के साथ Node.js 20 या उससे नए वर्ज़न पर चलता है।
MCP सर्वर क्या करता है
Model Context Protocol (MCP) एक ओपन स्टैंडर्ड है, जो किसी AI क्लाइंट को, जैसे Claude Code, Claude Desktop या किसी IDE एजेंट को, आपकी प्रोसेस के भीतर मौजूद फ़ंक्शन कॉल करने और डेटा पढ़ने देता है। मैसेज JSON-RPC 2.0 में आते-जाते हैं। आपका सर्वर बताता है कि वह क्या ऑफ़र करता है, क्लाइंट उन क्षमताओं की सूची देखता है, और मॉडल तय करता है कि उनका उपयोग कब करना है। आपका कोड सीधे मॉडल से बात नहीं करता। वह केवल रिक्वेस्ट का जवाब देता है, और इसी वजह से सर्वर छोटा रहता है।
तीन बिल्डिंग ब्लॉक
हर MCP सर्वर इन तीन प्रिमिटिव्स के किसी मिश्रण से बना होता है:
ब्लॉक
उपयोग का फ़ैसला कौन करता है
आम उपयोग
Tool
मॉडल
डेटाबेस से पूछताछ करना, API कॉल करना, इमेज जनरेट करना
Resource
एप्लिकेशन या यूज़र
कोई दस्तावेज़, फ़ाइल या कॉन्फ़िग रीडेबल कॉन्टेक्स्ट के रूप में उपलब्ध कराना
Prompt
यूज़र
एक दोबारा इस्तेमाल होने वाला टेम्पलेट, जैसे "इस पull request की समीक्षा करें"
व्यवहार में ज़्यादातर काम टूल करते हैं। टूल एक नाम वाला फ़ंक्शन है, जिसका एक टाइप्ड इनपुट स्कीमा होता है और जिसका नतीजा टेक्स्ट या इमेज हो सकता है। रिसोर्स और प्रॉम्प्ट वैकल्पिक हैं, लेकिन सर्वर बन जाने के बाद इन्हें जोड़ने की लागत लगभग शून्य होती है।
क्लाइंट, सर्वर और ट्रांसपोर्ट
ट्रांसपोर्ट केवल वह पाइप है जिससे JSON-RPC मैसेज आते-जाते हैं। वही McpServer ऑब्जेक्ट stdio या HTTP दोनों पर काम करता है, इसलिए सही ढाँचा यह है कि सर्वर को एक फ़ंक्शन में बनाएँ और ट्रांसपोर्ट को एक अलग एंट्री फ़ाइल में जोड़ें। नीचे दिया टेम्पलेट इसी नियम का पालन करता है, जिससे टेस्ट सरल रहते हैं और एक ही कोडबेस से दोनों ट्रांसपोर्ट भेजे जा सकते हैं।
TypeScript प्रोजेक्ट सेट अप करें
SDK और zod इंस्टॉल करें
एक फ़ोल्डर बनाएँ और डिपेंडेंसी इंस्टॉल करें। SDK इनपुट स्कीमा के लिए zod इस्तेमाल करता है: यह उन्हें क्लाइंट के लिए JSON Schema में बदलता है और आपके हैंडलर चलने से पहले हर आने वाले आर्गुमेंट को वैलिडेट करता है।
mcp-notes-server/
src/
index.ts stdio transport and startup
http.ts Streamable HTTP transport
server.ts buildServer() factory
package.json
tsconfig.json
💡 SDK इंपोर्ट .js पर खत्म होते हैं, TypeScript फ़ाइलों में भी।Node16 रिज़ॉल्यूशन के साथ कंपाइलर स्पष्ट एक्सटेंशन माँगता है, और अगर वह छूट जाए तो रनटाइम पर यह ERR_MODULE_NOT_FOUND के रूप में दिखता है।
सर्वर टेम्पलेट बनाएँ
सर्वर फ़ैक्टरी
सर्वर जो कुछ भी ऑफ़र करता है, उसे src/server.ts में रखें। इस संस्करण में एक टूल रजिस्टर होता है जो नोट सेव करता है और एक जो नोट खोजता है:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const notes = new Map<string, string>();
export function buildServer(): McpServer {
const server = new McpServer({ name: "notes-server", version: "1.0.0" });
server.registerTool(
"add_note",
{
title: "Add note",
description: "Save a short note under a unique id. Overwrites an existing id.",
inputSchema: {
id: z.string().min(1).describe("Unique id, for example 'standup-0612'"),
text: z.string().min(1).max(2000).describe("The note body"),
},
},
async ({ id, text }) => {
notes.set(id, text);
return { content: [{ type: "text", text: `Saved note ${id}` }] };
}
);
server.registerTool(
"get_note",
{
title: "Get note",
description: "Return the text of a saved note by id.",
inputSchema: { id: z.string().min(1).describe("The note id") },
},
async ({ id }) => {
const text = notes.get(id);
if (text === undefined) {
return { isError: true, content: [{ type: "text", text: `No note with id ${id}` }] };
}
return { content: [{ type: "text", text }] };
}
);
// resources and prompts go here (next section)
return server;
}
दो बातें दिखने से ज़्यादा मायने रखती हैं। पहली, description वह है जिसे मॉडल पढ़ता है यह तय करने के लिए कि टूल कॉल करना है या नहीं, इसलिए उसे किसी सहकर्मी के लिए दस्तावेज़ की तरह लिखें। दूसरी, जब कुछ विफल हो, तो थ्रो करने के बजाय पढ़ने योग्य संदेश के साथ isError: true लौटाएँ। तब मॉडल सुधारे गए आर्गुमेंट के साथ दोबारा कोशिश कर सकता है या यूज़र को समस्या समझा सकता है।
एक रिसोर्स और एक प्रॉम्प्ट जोड़ें
प्लेसहोल्डर कमेंट की जगह यह डालें:
server.registerResource(
"all-notes",
"notes://all",
{
title: "All notes",
description: "Every saved note as JSON",
mimeType: "application/json",
},
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify([...notes.entries()]) }],
})
);
server.registerPrompt(
"summarize-notes",
{
title: "Summarize notes",
description: "Ask for a short summary of the saved notes",
argsSchema: { tone: z.string().optional() },
},
({ tone }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Summarize my saved notes in a ${tone ?? "neutral"} tone.`,
},
},
],
})
);
रिसोर्स URI (notes://all) से एड्रेस किए जाते हैं, और क्लाइंट आम तौर पर उन्हें एक पिकर में दिखाते हैं, ताकि यूज़र उन्हें कॉन्टेक्स्ट के रूप में अटैच कर सके। प्रॉम्प्ट क्लाइंट के आधार पर slash commands या मेनू एंट्री के रूप में दिखते हैं।
एक कोडिंग मॉडल से मदद लें
एक बार यह टेम्पलेट चल जाए, तो कोडिंग मॉडल मिनटों में नए टूल जोड़ सकता है। src/server.ts को चैट में पेस्ट करें और उसी पैटर्न पर चलने वाला नया टूल माँगें: पहले स्कीमा, विफलता पर isError। Claude Sonnet 5, Kimi K2.6 और GPT 5.6 Sol सभी कोड के काम के लिए बने हैं और PicassoIA पर उपलब्ध हैं। जेनरेट हुआ स्कीमा स्वीकार करने से पहले उसे पढ़ें: मॉडल किसी ऐसे फ़ील्ड को आसानी से वैकल्पिक बना देगा जो अनिवार्य होना चाहिए।
ट्रांसपोर्ट चुनें
स्थानीय क्लाइंट के लिए stdio
stdio सबसे सरल ट्रांसपोर्ट है। क्लाइंट आपके सर्वर को एक चाइल्ड प्रोसेस के रूप में लॉन्च करता है और stdin व stdout के ज़रिए उससे बात करता है। src/index.ts बनाएँ:
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { buildServer } from "./server.js";
const server = buildServer();
await server.connect(new StdioServerTransport());
console.error("notes-server ready on stdio");
💡 stdio सर्वर में कभी console.log इस्तेमाल न करें। Stdout में प्रोटोकॉल जाता है, इसलिए एक भटकी हुई लॉग लाइन स्ट्रीम को खराब कर देती है और क्लाइंट parse error के साथ डिस्कनेक्ट हो जाता है। लॉग console.error के ज़रिए stderr पर भेजें।
रिमोट क्लाइंट के लिए Streamable HTTP
जो सर्वर लैपटॉप के बजाय किसी होस्ट पर रहता है, उसके लिए Streamable HTTP इस्तेमाल करें। इसने पुराने HTTP plus SSE ट्रांसपोर्ट की जगह ली है और इसे एक ही एंडपॉइंट चाहिए। npm install express और npm install -D @types/express से Express इंस्टॉल करें, फिर इसे src/http.ts के रूप में सेव करें:
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./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, () => console.error("MCP endpoint on http://localhost:3000/mcp"));
sessionIdGenerator: undefined सेट करने से ट्रांसपोर्ट stateless मोड में चलता है: हर रिक्वेस्ट के लिए नया सर्वर, ट्रैक करने के लिए कोई सेशन नहीं, और किसी भी दूसरे API की तरह हॉरिज़ॉन्टल स्केलिंग। अगर आपको सर्वर-इनिशिएटेड नोटिफ़िकेशन या रिज़्यूमेबल स्ट्रीम चाहिए, तो सेशन ids के साथ stateful मोड पर जाएँ।
stdio
Streamable HTTP
कहाँ चलता है
क्लाइंट की चाइल्ड प्रोसेस
HTTP से पहुँचने योग्य कोई भी होस्ट
ऑथेंटिकेशन
यूज़र के एनवायरनमेंट से लिया जाता है
आप जोड़ते हैं (OAuth या bearer tokens)
सबसे उपयुक्त
व्यक्तिगत और स्थानीय डेवलपर टूल
साझा और होस्टेड सर्वर
स्केलिंग
हर क्लाइंट के लिए एक प्रोसेस
Stateless, API की तरह स्केल करें
कनेक्ट करने से पहले टेस्ट करें
MCP Inspector चलाएँ
Inspector आधिकारिक डीबगिंग UI है। प्रोजेक्ट बिल्ड करें और उसके ज़रिए अपना सर्वर लॉन्च करें:
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
वह लोकल URL खोलें जो वह प्रिंट करता है, Connect दबाएँ, फिर Tools टैब से टूल्स की सूची देखें और एक JSON आर्गुमेंट के साथ add_note चलाएँ। हिस्ट्री पेन कच्चा JSON-RPC ट्रैफ़िक दिखाता है, और किसी ऐसे स्कीमा को पकड़ने का सबसे तेज़ तरीका है जो आपके इरादे से मेल नहीं खाता।
Claude Code और Claude Desktop में रजिस्टर करें
Claude Code एक कमांड से लोकल सर्वर रजिस्टर करता है, और HTTP सर्वर एक ट्रांसपोर्ट फ़्लैग से:
claude mcp add notes -- node /absolute/path/mcp-notes-server/dist/index.js
claude mcp add --transport http notes-remote http://localhost:3000/mcp
Claude Desktop इसके बजाय एक JSON फ़ाइल पढ़ता है (claude_desktop_config.json):
💡 दोनों जगह absolute paths का इस्तेमाल करें। रिलेटिव पाथ क्लाइंट की वर्किंग डायरेक्टरी के हिसाब से हल होता है, आपकी के हिसाब से नहीं, और एरर मैसेज शायद ही यह बताता है।
इमेज और वीडियो टूल जोड़ें
नोट्स वाला सर्वर पैटर्न साबित करता है। मीडिया टूल दिखाते हैं कि यह क्यों फ़ायदेमंद है: धीमे जॉब, बड़े आउटपुट और एक बाहरी API जिसकी अपनी सीमाएँ हैं। PicassoIA https://api.picassoia.com/v1 पर Replicate-style डेवलपर API देता है, जो उस टोकन से प्रमाणित होता है जो pia_sk_ से शुरू होता है। API और MCP कनेक्टर दोनों से चार मॉडल पहुँच में हैं: टेक्स्ट-टू-इमेज के लिए PicassoIA Image, एडिट के लिए PicassoIA Image Editor Pro, वीडियो के लिए PicassoIA Video, और ऑडियो के साथ वीडियो के लिए Seedance 2.5 Lite। जॉब एसिंक्रोनस होते हैं: आप एक prediction बनाते हैं, उसे poll करते हैं, फिर नतीजा पढ़ते हैं।
एक इमेज एंडपॉइंट को रैप करें
दो छोटे helpers हर मॉडल के लिए काम करते हैं, क्योंकि API का शेप एक जैसा है:
const API = "https://api.picassoia.com/v1";
const headers = {
Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
"Content-Type": "application/json",
};
export async function createPrediction(model: string, input: Record<string, unknown>) {
const res = await fetch(`${API}/models/${model}/predictions`, {
method: "POST",
headers,
body: JSON.stringify({ input }),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) throw new Error(`Create failed: HTTP ${res.status}`);
return (await res.json()) as { id: string };
}
export async function getPrediction(id: string) {
const res = await fetch(`${API}/predictions/${id}`, { headers });
if (!res.ok) throw new Error(`Status failed: HTTP ${res.status}`);
return (await res.json()) as { status: string; output?: unknown; error?: string };
}
इमेज टूल इन्हें इस्तेमाल करता है और नतीजे के लिए अधिकतम दो मिनट तक इंतज़ार करता है:
server.registerTool(
"generate_image",
{
title: "Generate image",
description: "Create an image from a text prompt and return its URL.",
inputSchema: { prompt: z.string().min(10).max(4000) },
},
async ({ prompt }) => {
try {
const { id } = await createPrediction("picassoia/picassoia-image", { prompt });
for (let i = 0; i < 60; i++) {
const job = await getPrediction(id);
if (job.status === "succeeded") {
return { content: [{ type: "text", text: JSON.stringify(job.output) }] };
}
if (job.status === "failed") throw new Error(job.error ?? "Generation failed");
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Timed out waiting for the image");
} catch (err) {
return { isError: true, content: [{ type: "text", text: String(err) }] };
}
}
);
स्कीमा में 4,000 कैरेक्टर की सीमा API की प्रॉम्प्ट लिमिट से मेल खाती है, और हर मॉडल का पेज अपने सटीक इनपुट फ़ील्ड और आउटपुट फ़ॉर्मेट की सूची देता है। API प्रति अकाउंट 5 concurrent predictions की भी अनुमति देता है, जो सभी टोकन और MCP कनेक्शन में साझा होते हैं, इसलिए समानांतर टूल कॉल्स को एक साथ न चलाएँ, बल्कि उन्हें कतार में लगाएँ।
धीमे वीडियो जॉब संभालें
वीडियो में इमेज से कहीं ज़्यादा समय लगता है, और कोई टूल कॉल जो मिनटों तक ब्लॉक रहे, क्लाइंट के अपने टाइमआउट से टकरा सकता है। काम को दो टूल्स में बाँटें: एक जॉब शुरू करता है और तुरंत उसका id लौटाता है, दूसरा उसकी जाँच करता है।
server.registerTool(
"start_video",
{
title: "Start video",
description: "Start a video job from a prompt. Returns a prediction id to check later.",
inputSchema: { prompt: z.string().min(10).max(4000) },
},
async ({ prompt }) => {
const { id } = await createPrediction("picassoia/picassoia-video", { prompt });
return { content: [{ type: "text", text: JSON.stringify({ predictionId: id }) }] };
}
);
server.registerTool(
"check_video",
{
title: "Check video",
description: "Return the status and output of a video job by prediction id.",
inputSchema: { predictionId: z.string().min(1) },
},
async ({ predictionId }) => {
const job = await getPrediction(predictionId);
return { content: [{ type: "text", text: JSON.stringify(job) }] };
}
);
मॉडल start_video कॉल करता है, फिर कोई और काम करता है, और जब तक स्टेटस succeeded न दिखाए, तब तक check_video को poll करता है। कुछ भी ब्लॉक नहीं होता, और विफल जॉब भी बस रिपोर्ट करने लायक एक और स्टेटस होता है। ऑडियो के साथ वीडियो के लिए मॉडल को picassoia/seedance-2.5-lite (Seedance 2.5 Lite) पर स्विच करें; helpers नहीं बदलते।
सुरक्षित रूप से शिप करें
इनपुट वैलिडेट करें और सीक्रेट्स सुरक्षित रखें
हर टूल आर्गुमेंट को अविश्वसनीय मानें। मॉडल को उस टेक्स्ट से बहकाया जा सकता है जो वह वेब या किसी फ़ाइल में पढ़ता है, इसलिए कोई इंजेक्टेड पेज आपके टूल से वह करवा सकता है जो आपने कभी नहीं चाहा। तीन आदतें ज़्यादातर जोखिम कम कर देती हैं:
zod में हर फ़ील्ड पर सीमा तय करें: min, max, enum और आइडी के लिए regex।
आर्गुमेंट्स को कभी शेल कमांड या SQL स्ट्रिंग में न डालें। पैरामीटराइज़्ड क्वेरी का इस्तेमाल करें और फ़ाइल पाथ को एक ही बेस डायरेक्टरी तक सीमित रखें।
टोकन एनवायरनमेंट वेरिएबल से पढ़ें, उन्हें कभी हार्ड-कोड न करें, और किसी टूल रिज़ल्ट में उन्हें कभी वापस न दोहराएँ।
stdio क्लाइंट के लिए सीक्रेट्स क्लाइंट कॉन्फ़िग के env ब्लॉक में सेट करें। HTTP सर्वर के लिए Authorization हेडर अनिवार्य करें और handleRequest चलने से पहले उसकी जाँच करें।
तीन आम गलतियाँ सुधारें
stdio पर console.log। इसे console.error पर स्विच करें।
.js एक्सटेंशन गायब।./server जैसा इंपोर्ट Node16 के अंतर्गत रनटाइम पर विफल होता है।
अस्पष्ट descriptions। "does stuff" वाले description के साथ run नाम का टूल कभी चुना नहीं जाता, या गलत आर्गुमेंट के साथ चुना जाता है। उसका नाम उसके काम के आधार पर रखें और बताएँ कि उसे कब इस्तेमाल करना है।
प्रकाशित करने के लिए src/index.ts के सबसे ऊपर shebang लाइन रखें, npm run build चलाएँ, फिर npm publish। कोई भी claude mcp add notes -- npx -y mcp-notes-server से उसे रजिस्टर कर सकता है। HTTP सर्वर कंटेनर के रूप में शिप होते हैं या किसी भी Node होस्ट पर चलते हैं।
Picasso IA पर आज़माएँ
अब आपके पास एक ऐसा टेम्पलेट है जो स्थानीय रूप से चलता है, रिमोट रूप से चलता है, और इमेज व वीडियो मॉडल को कॉल कर सकता है। यह देखने का सबसे तेज़ तरीका कि वे टूल क्या लौटाते हैं, पहले मॉडल को खुद आज़माना है। PicassoIA Image खोलें और ऐसा प्रॉम्प्ट लिखें जितना साफ़-साफ़ आप generate_image से भेजते: सब्जेक्ट, लेंस, रोशनी, सेटिंग। फिर PicassoIA Video या Seedance 2.5 Lite से विचार को एनिमेट करके देखें, और अपने सर्वर में डिफ़ॉल्ट हार्ड-कोड करने से पहले नोट करें कि कौन-सा वाक्यांश आपको वांछित नतीजा देता है। पूरे कैटलॉग से कोई भी मॉडल picassoia.com/en/all-models पर चुनें और उसे उन्हीं दो helpers से जोड़ें। आज ही Picasso IA पर अपनी इमेज बनाएँ, और आपका पहला MCP टूल कॉल आपको नतीजा दे दे।