Бұл нұсқаулық жазбалар сервисіне арналған шағын қашықтағы MCP серверін құрады. Мысал MCP 2026-07-28 нұсқасын іске асыратын TypeScript SDK-тің тұрақты v2 тармағын қолданады. Сервер заманауи Streamable HTTP сұрауларын қабылдайды, OAuth access token-ін ресурс сервері ретінде тексереді, оқу және жою құралдарын ұсынады, аргументтерді Zod арқылы тексереді және рұқсат тексеруін қорғалатын операцияның қасына қояды. Код іске қосылатын мысал, бірақ бұл мақала оның сіздің ортаңызда іске қосылғанын мәлімдемейді.
Протокол нұсқасы маңызды. v2 пакеттері @modelcontextprotocol/server, @modelcontextprotocol/client, @modelcontextprotocol/core және орындау ортасының адаптерлеріне бөлінген. Жаңа сервиске @modelcontextprotocol/sdk/server/mcp.js импорттайтын ескі мысалдарды көшірмеңіз. Сымдағы және пакеттердегі өзгерістер MCP 2026-07-28 көшіру нұсқаулығында толық түсіндірілген.
Алдымен заманауи сым моделін түсініңіз
Streamable HTTP бір MCP нүктесін, әдетте /mcp, пайдаланады және клиенттің әрбір JSON-RPC сұрауын жеке HTTP POST ретінде жібереді. Жауап бір JSON нысаны немесе сол сұрауға арналған SSE ағыны болады. Хабарлама 202 мәртебесімен, денесіз расталады. Клиент Accept ішінде application/json және text/event-stream екі түрін де жариялайды, ал JSON-ды Content-Type: application/json арқылы жібереді.
2026-07-28 нұсқасы ескі GET ағынын, протокол деңгейіндегі сессия идентификаторларын және Last-Event-ID тарихы арқылы жалғастыруды алып тастайды. Ол серверден клиентке жіберілетін тәуелсіз JSON-RPC сұрауларын да жояды. Құралға растау немесе басқа мән қажет болса, ол input_required нәтижесін қайтарады. Клиент ендірілген сұрауға жауап беріп, бастапқы шақыруды қайталайды. Ұзақ өмір сүретін тізім өзгерісі туралы хабарламалар жалпы GET қосылымымен емес, subscriptions/listen жауап ағынымен жеткізіледі.
Әр заманауи POST-та MCP-Protocol-Version болады, оның мәні JSON денесіндегі _meta.io.modelcontextprotocol/protocolVersion мәнімен бірдей болуы керек. Mcp-Method әр сұраудың JSON-RPC әдісін қайталайды. tools/call, resources/read және prompts/get сұрауларында Mcp-Name params.name немесе params.uri мәнін қайталайды. Gateway бұл тақырыптарды бағыттау үшін қолдана алады, бірақ қолданба тақырып пен дене сәйкес келмесе бәрібір қабылдамауы керек. Дене шындықтың негізгі көзі болып қалады.
Заманауи ең аз шақыру мынадай:
POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: notes.search
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"notes.search","arguments":{"query":"gateway"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"notes-cli","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}
v2 SDK орнатыңыз
Node.js 20 немесе одан жаңасын, TypeScript 6-ны және SDK пакеттерін пайдаланып ESM Node.js жобасын жасаңыз. Нұсқаларды қолданбаның lockfile файлында бекітіңіз. Осы мақаланы тексерген кезде тұрақты сервер пакеті мен Node және Express адаптерлерінің нұсқасы 2.0.0 болды. Zod схемаларын қолданғанда сервер пакетіне Zod 4 қажет.
npm install @modelcontextprotocol/[email protected] @modelcontextprotocol/[email protected] @modelcontextprotocol/[email protected] express@5 zod@4
npm install --save-dev typescript@6 @types/node @types/express tsx
TypeScript 6-та конфигурацияда жоқ болса, Node.js типтерін ашық түрде қосыңыз:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"types": ["node"]
}
}
Сервер мен ресурс серверінің қорғанысын жасаңыз
createMcpHandler factory қабылдап, әр HTTP сұрауына жаңа McpServer жасайды. toNodeHandler оның web-standard fetch интерфейсін Node сұрауы мен жауабының нысандарына бейімдейді. Express адаптері JSON талдауды, бапталған host үшін host және origin қорғанысын, сондай-ақ OAuth middleware-ін береді.
MCP сервері OAuth resource server болып табылады. Ол authorization server шығарған токендерді тексереді, бірақ токен шығармайды. Төмендегі verifier RFC 7662 introspection қолданады. Ол әдейі active, sub және exp мәндерін тексеріп, бос орынмен бөлінген scope өрісін талдайды және SDK-тің AuthInfo мәнін қайтарады. Жергілікті JWT тексеруіне issuer, audience, қолтаңба, кілт, сағат саясаты және кілтті айналдыру іске асырылып, тексерілгеннен кейін ғана ауысыңыз.
import {
createMcpExpressApp,
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
requireBearerAuth,
type OAuthTokenVerifier
} from "@modelcontextprotocol/express";
import { toNodeHandler } from "@modelcontextprotocol/node";
import {
createMcpHandler,
McpServer,
OAuthError,
OAuthErrorCode,
type AuthInfo,
type OAuthMetadata
} from "@modelcontextprotocol/server";
import type { Request, Response } from "express";
import * as z from "zod/v4";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
const host = process.env.HOST ?? "127.0.0.1";
const mcpUrl = new URL(required("MCP_URL"));
const scopes = ["mcp", "notes:read", "notes:write"];
const oauthMetadata: OAuthMetadata = {
issuer: required("OAUTH_ISSUER"),
authorization_endpoint: required("OAUTH_AUTHORIZATION_ENDPOINT"),
token_endpoint: required("OAUTH_TOKEN_ENDPOINT"),
response_types_supported: ["code"],
scopes_supported: scopes,
code_challenge_methods_supported: ["S256"],
authorization_response_iss_parameter_supported: true
};
const notes = [
{ id: "1", text: "Rotate the signing key after the release." },
{ id: "2", text: "Review the MCP gateway rate limit." }
];
async function verifyAccessToken(token: string): Promise<AuthInfo> {
const credentials = Buffer.from(`${required("OAUTH_CLIENT_ID")}:${required("OAUTH_CLIENT_SECRET")}`).toString("base64");
const response = await fetch(required("OAUTH_INTROSPECTION_URL"), {
method: "POST",
headers: {
authorization: `Basic ${credentials}`,
"content-type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({ token }).toString()
});
if (!response.ok) throw new OAuthError(OAuthErrorCode.InvalidToken, "Token introspection failed");
const payload = await response.json() as {
active?: unknown;
sub?: unknown;
client_id?: unknown;
scope?: unknown;
exp?: unknown;
};
if (payload.active !== true || typeof payload.sub !== "string" || typeof payload.exp !== "number") {
throw new OAuthError(OAuthErrorCode.InvalidToken, "Token is inactive or incomplete");
}
const tokenScopes = typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [];
return {
token,
clientId: typeof payload.client_id === "string" ? payload.client_id : payload.sub,
scopes: tokenScopes,
expiresAt: payload.exp
};
}
const verifier: OAuthTokenVerifier = { verifyAccessToken };
function buildServer(): McpServer {
const server = new McpServer({ name: "notes", version: "1.0.0" });
server.registerTool(
"notes.search",
{
title: "Search notes",
description: "Find notes containing a phrase.",
inputSchema: z.object({
query: z.string().trim().min(1).max(200),
limit: z.number().int().min(1).max(20).default(10)
})
},
async ({ query, limit }, ctx) => {
if (!ctx.http?.authInfo?.scopes.includes("notes:read")) {
return { content: [{ type: "text", text: "insufficient_scope: notes:read is required" }], isError: true };
}
const needle = query.toLocaleLowerCase();
const matches = notes.filter(note => note.text.toLocaleLowerCase().includes(needle)).slice(0, limit);
return { content: [{ type: "text", text: JSON.stringify(matches) }] };
}
);
server.registerTool(
"notes.delete",
{
title: "Delete a note",
description: "Delete one note by id.",
inputSchema: z.object({ id: z.string().regex(/^\d+$/) })
},
async ({ id }, ctx) => {
if (!ctx.http?.authInfo?.scopes.includes("notes:write")) {
return { content: [{ type: "text", text: "insufficient_scope: notes:write is required" }], isError: true };
}
const index = notes.findIndex(note => note.id === id);
if (index === -1) return { content: [{ type: "text", text: "note not found" }], isError: true };
const [removed] = notes.splice(index, 1);
return { content: [{ type: "text", text: `deleted ${removed.id}` }] };
}
);
return server;
}
const handler = createMcpHandler(buildServer);
const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(mcpUrl);
const app = createMcpExpressApp({ host, allowedHosts: [mcpUrl.hostname], jsonLimit: "64kb" });
app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpUrl, scopesSupported: scopes, resourceName: "Notes MCP" }));
const auth = requireBearerAuth({ verifier, requiredScopes: ["mcp"], resourceMetadataUrl });
const node = toNodeHandler(handler);
app.all(mcpUrl.pathname, auth, (req: Request, res: Response) => {
void node(req, res, req.body);
});
const port = mcpUrl.port ? Number(mcpUrl.port) : Number(process.env.PORT ?? 3000);
app.listen(port, host);
mcp scope endpoint деңгейіндегі қақпа болады. Екі құралдың ішіндегі тексерістер одан да нақты бөлу береді. Тек mcp алған клиент endpoint-ті таба алады, бірақ қорғалған операцияны шақырғанда isError: true бар кәдімгі құрал нәтижесін алады. Бас тарту модельге көрінеді. Бүкіл endpoint-ке scope керек болса, оны requiredScopes ішіне қосыңыз. Middleware insufficient_scope бар 403 қайтарады.
Metadata router RFC 9728 Protected Resource Metadata жариялап, RFC 8414 authorization-server metadata мәнін көрсетеді. Клиент WWW-Authenticate challenge ішіндегі resource_metadata URL-іне өтіп, issuer мен endpoint-терді тауып, токен алып, сұрауды қайталай алады. TypeScript SDK сіздің identity provider-іңізді іске асырмайды. Жаңа сервис үшін қолдау көрсетілетін identity provider немесе OAuth server қолданыңыз. @modelcontextprotocol/server-legacy/auth ішіндегі v1 authorization-server helper-лері көшіру үшін қатырып қойылған, сондықтан жаңа кодқа ұсынылмайды.
2026-07-28 авторизация ережелері клиент жағын да күшейтеді. Authorization server авторизация жауаптарында iss қайтаруы керек. Клиент code-ты token endpoint-ке жібермес бұрын оны discovery кезінде сақталған issuer мәнімен салыстырады. Client credentials пен токендер issuer бойынша бөлініп сақталуы тиіс. Client ID Metadata Documents Dynamic Client Registration-нан артық, ал DCR тек үйлесімділік үшін қалды. Бұл authorization-server mix-up салдарынан бір сервердің code немесе token-і басқа сервердің credential-іне айналуына жол бермейді.
Stateless режимді ашық көрсетіңіз
Factory әр сұрауға сервер жасайды, сондықтан аутентификацияланған caller-ді, workflow кезеңін немесе рұқсат шешімін instance ішінде сақтап, келесі POST-та қайта табылады деп күтпеңіз. Заманауи сұрау кәдімгі round-robin load balancer артындағы кез келген replica-ға баруы мүмкін. Тұрақты бизнес күйін дерекқорда немесе кезекте сақтап, тексерілген қысқа мерзімді handle-ді құрал аргументі ретінде беріңіз.
Растау ағынында input_required қайтарып, оның күйін қорғаңыз. Бұл үлгіге SDK inputRequired, acceptedContent және createRequestStateCodec экспорттайды:
import { acceptedContent, createRequestStateCodec, inputRequired, McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";
function buildServerWithConfirmation(): McpServer {
const key = process.env.REQUEST_STATE_KEY;
if (!key) throw new Error("Missing REQUEST_STATE_KEY");
type DeleteState = { noteId: string; step: "confirm" };
const stateCodec = createRequestStateCodec<DeleteState>({
key,
ttlSeconds: 300,
bind: ctx => `${ctx.mcpReq.method}\u0000${ctx.http?.authInfo?.clientId ?? ""}`
});
const server = new McpServer(
{ name: "notes-confirmation", version: "1.0.0" },
{ requestState: { verify: stateCodec.verify } }
);
const notes = [{ id: "1", text: "Rotate the signing key after the release." }];
const confirmationSchema = z.object({ confirm: z.boolean() });
server.registerTool(
"notes.delete",
{ inputSchema: z.object({ id: z.string().regex(/^\d+$/) }) },
async ({ id }, ctx) => {
const confirmed = acceptedContent(ctx.mcpReq.inputResponses, "confirm", confirmationSchema);
if (!confirmed) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: "Delete this note?",
requestedSchema: confirmationSchema
})
},
requestState: await stateCodec.mint({ noteId: id, step: "confirm" }, ctx)
});
}
if (!confirmed.confirm) return { content: [{ type: "text", text: "deletion declined" }], isError: true };
const index = notes.findIndex(note => note.id === id);
if (index === -1) return { content: [{ type: "text", text: "note not found" }], isError: true };
notes.splice(index, 1);
return { content: [{ type: "text", text: `deleted ${id}` }] };
}
);
return server;
}
stateCodec.verify мәнін McpServer-ге берілетін requestState.verify опциясына қосыңыз. Күйге қол қойылады, бірақ шифрланбайды. SDK client қайтарған күйді осы hook тексергенге дейін сенімсіз деп қабылдайды. Оны principal мен операцияға байланыстырыңыз, мерзімін белгілеңіз және payload ішіне құпия салмаңыз. Retry қабылдай алатын барлық replica ортақ HMAC кілтін қолдануы керек.
Валидация жасаңыз, шектеңіз және орналастырыңыз
Zod валидациясы қолданбаның бірінші шекарасы, бірақ қауіпсіздік моделінің барлығы емес. Жолдардың, массивтердің, сандардың, ID, URL, жолдардың және нәтижелердің өлшемін шектеңіз. Downstream API шекарасында қайта тексеріңіз, өйткені schema пішінді ғана дәлелдейді, рұқсатты немесе жанама әсер қауіпсіздігін емес. Модельге еркін HTTP әдісін, host, filesystem path, SQL үзіндісін немесе shell argument таңдатпаңыз. Allowlist және параметрленген API қолданыңыз.
Rate limiting негізгі кілт ретінде аутентификацияланған clientId немесе tenant қолдануы керек. Аутентификация қателері, discovery, қымбат құралдар және параллель операциялар үшін бөлек шектер қойыңыз. Edge немесе gateway ішінде Retry-After бар 429 қайтарыңыз. Процестегі санауыш бір әзірлеу процесіне жарайды, бірақ кластерге ортақ шек емес, ал шектелмеген map жадыға қысым түсіруі мүмкін. Бірнеше replica үшін Redis сияқты ортақ limiter немесе gateway саясатын қолданыңыз. Метрикаларға тексерілген тақырыптардан method және tool name енгізіңіз, бірақ bearer token-ді ешқашан журналға жазбаңыз.
TLS-ті ашық endpoint алдында аяқтаңыз немесе end-to-end TLS қолданыңыз, SSE ағынын сақтаңыз және proxy қолдаса X-Accel-Buffering: no арқылы буферлеуді өшіріңіз. Body, header, idle және upstream timeout мәндерін әдейі орнатыңыз. Жергілікті серверді 0.0.0.0 емес, 127.0.0.1-ге байлаңыз, тек allowlist пен authentication қорғаса ғана өзгеше таңдаңыз. Express factory host және origin қорғанысын тек бапталған localhost класы үшін қосады. Public bind кезінде allowedHosts пен allowedOrigins мәндерін нақты көрсетіңіз.
Заманауи протоколға sticky sessions қажет емес. subscriptions/listen ағыны бөлек: өзгеріс хабарламалары replica арасында қажет болса, pub/sub үстінде ортақ ServerEventBus беріңіз. Дерекқор пулдары мен кэштерді модуль деңгейінде сақтаңыз, ал caller-ге тән шешімдерді request context ішінде ұстаңыз. Graceful shutdown кезінде handler.close() шақырыңыз, сонда жүріп жатқан заманауи алмасулар процесс шыққанға дейін тоқтайды.
Тек функцияны емес, сымды да тестілеңіз
v2 SDK createMcpHandler-ді арнайы fetch функциясымен StreamableHTTPClientTransport арқылы шақыратын in-process тест жолын құжаттайды. Нақты request/response жолын қолданыңыз, сонда version negotiation, міндетті тақырыптар, JSON және SSE жауаптары, authentication өтуі, schema қателері, cancellation және input_required retry тексеріледі. Token жоқ 401, inactive token үшін 401, endpoint scope жоқ 403 және құрал scope-ы жоқ кездегі in-band isError бас тартуына бөлек тест қосыңыз.
Modern wire тестінде mode: { pin: "2026-07-28" } қолданыңыз. Үйлесімділік тестінде mode: "auto" қолданып, әдейі ескі fixture арқылы legacy fallback тексеріңіз. Әртүрлі caller-мен екі сұрау жіберіп, құрал көрінуі мен авторизация бірінен біріне өтпегенін растаңыз. Multi-node тестінде retry-ді басқа процеске жіберіп, ортақ state codec пен durable store workflow-ды сақтайтынын тексеріңіз.
Бірінші істен шыққан қабаттан бастап ақауды табыңыз
HeaderMismatch бар HTTP 400 protocol header мен _meta claim сәйкес келмейтінін немесе міндетті routing header жоқ екенін білдіреді. Client не gateway-ді түзетіңіз және дене танылған заманауи қате болса ескі протоколға үнсіз ауыспаңыз. HTTP 415 POST media type application/json емес екенін білдіреді. text/plain; a=application/json тәрізді мән жарамсыз. Modern endpoint үшін GET-тегі 404 күтілетін нәтиже. POST-тегі 404 белгісіз method немесе MCP handler-ге жетпеген маршрут болуы мүмкін.
401 жауапта WWW-Authenticate challenge болуы керек. Metadata URL, token issuer, audience немесе resource, қолтаңба не introspection және сандық expiration claim мәндерін тексеріңіз. SDK bearer gate expiresAt жоқ AuthInfo мәнін қабылдамайды. 403 insufficient_scope endpoint деңгейіндегі scope challenge екенін білдіреді және клиенттің step-up flow-ын бастай алады. isError: true бар құрал бас тартуы қолданба нәтижесі, ол автоматты түрде кең token бермейді.
Клиент тұрып қалса, URL-ді, reverse proxy SSE-ні буферлейтінін, idle timeout-ты және сол JSON-RPC ID үшін жауап келгенін тексеріңіз. Modern client legacy server көрсе, versionNegotiation auto немесе pin екенін, SDK нұсқалары бірдей екенін және gateway _meta не міндетті тақырыптарды алып тастамағанын тексеріңіз. Confirmation шексіз қайталанса, embedded response кілтін, inputResponses валидациясын, rounds шегін және қол қойылған state кезеңін тексеріңіз. Көшіру контексті мен үйлесімділік матрицасы үшін MCP 2026-07-28 көшіру нұсқаулығын қараңыз. MCP айналасындағы injection, tool poisoning және authorization қауіптері prompt injection және MCP қауіпсіздігі материалында түсіндірілген. Сервис шекаралары, кезектер және ақауды оқшаулау үшін өндірістік AI agent архитектурасын оқыңыз.
Дереккөздер
- MCP Streamable HTTP спецификациясы, 2026-07-28
- MCP авторизация спецификациясы, 2026-07-28
- TypeScript SDK v2 анықтамалығы
createMcpHandlerAPI анықтамалығыrequireBearerAuthAPI анықтамалығы- TypeScript SDK-тің 2026-07-28 қолдау нұсқаулығы
- RFC 7662 OAuth 2.0 Token Introspection
- RFC 9207 authorization-server issuer идентификациясы
- RFC 9728 OAuth 2.0 Protected Resource Metadata