---
title: 'سيرفر MCP في TypeScript: OAuth 2.1 + Streamable HTTP (2026)'
description: ابنِ خادم MCP جاهز للإنتاج في TypeScript مع OAuth 2.1، ومؤشرات موارد RFC 8707، و Streamable HTTP، وتفويض أدوات مدرك للنطاق في عام 2026.
url: https://nerdleveltech.com/ar/mcp-server-typescript-oauth-streamable-http-production-tutorial
language: ar
date: '2026-05-14'
category: ai-ml
tags:
  - mcp
  - typescript
  - oauth
  - model context protocol
  - claude
  - api security
  - tutorial
markdown_url: https://nerdleveltech.com/ar/mcp-server-typescript-oauth-streamable-http-production-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/mcp-server-typescript-oauth-streamable-http-production-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# سيرفر MCP في TypeScript: OAuth 2.1 + Streamable HTTP (2026)

لإضافة OAuth 2.1 إلى خادم Model Context Protocol في TypeScript، قم بتشغيل `@modelcontextprotocol/sdk` الإصدار 1.29.0 و `StreamableHTTPServerTransport` خلف Express، وانشر `/.well-known/oauth-protected-resource` وفقًا لـ RFC 9728، وتحقق من صحة رموز bearer باستخدام `jose` مع فرض مؤشرات الموارد RFC 8707، وافحص `authInfo.scopes` داخل كل معالج أدوات (tool handler). يبني هذا الدليل العملية بالكامل من البداية إلى النهاية.

## ملخص

ستقوم ببناء خادم MCP عن بُعد في TypeScript يمكن لعميل MCP (مثل Claude Desktop أو Cursor أو أي وكيل متوافق مع المواصفات) استدعاؤه عبر HTTP باستخدام رمز bearer. يعرض الخادم API مهام (`tasks`) بنطاقين (scopes) — هما `tasks:read` و `tasks:write` — ويستعرض كل جزء من مواصفات تفويض MCP لشهر نوفمبر 2025[^1]: البيانات الوصفية للموارد المحمية، ومؤشرات الموارد، والتفويض القائم على النطاق داخل معالجات الأدوات، وتخفيف مخاطر DNS-rebinding الذي ظهر في SDK 1.24.0. بنهاية الدليل، سيكون لديك خادم قابل للتشغيل والمراقبة ويدرك النطاقات في حوالي 300 سطر من TypeScript موزعة على خمسة ملفات صغيرة.

## ما ستتعلمه

- كيفية هيكلة خادم MCP باستخدام `@modelcontextprotocol/sdk` 1.29.0 و `zod` 4.4.3
- كيفية تسجيل أدوات ذات أنواع محددة (typed tools) تتلقى معالجاتها وسيط `authInfo`
- كيفية ربط `StreamableHTTPServerTransport` خلف Express 5 مع إدارة الجلسات
- كيفية نشر `/.well-known/oauth-protected-resource` وفقًا لـ RFC 9728
- كيفية التحقق من رموز الوصول باستخدام `jose` وفرض مؤشرات الموارد RFC 8707
- كيفية فرض نطاقات OAuth لكل أداة داخل المعالج
- كيفية تفعيل الحماية من DNS-rebinding والقائمة البيضاء لرأس Host
- كيفية إضافة سجلات منظمة باستخدام `pino` بحيث تكون عمليات التشغيل في الإنتاج قابلة للتصحيح

## المتطلبات الأساسية

- Node.js 22 LTS أو Node.js 24 LTS. يتطلب SDK إصدار Node 18+؛ Node 22 في مرحلة الصيانة (Maintenance LTS) حتى أبريل 2027 و Node 24 هو الإصدار النشط الحالي (Active LTS) حتى أكتوبر 2026[^2]. الأمثلة أدناه تثبت Node 22 لتتوافق مع سطر `@types/node` المثبت في الخطوة 1؛ كل شيء في هذا الدليل يعمل دون تغيير على Node 24.
- npm 10 (يأتي مع Node 22) أو pnpm 10.
- خادم تفويض OAuth 2.1 يصدر رموز وصول JWT مع ادعاءات (claims) `aud`، و `iss`، و `sub`، و `scope`، و `exp`. تدعم WorkOS AuthKit و Stytch مؤشر الموارد RFC 8707 بشكل أصلي؛ وتدعمه Auth0 خلف ملف تعريف توافق معلمات الموارد الاختياري؛ أما Microsoft Entra ID فلا يطبق معلمة `resource` بعد ويتطلب حلاً بديلاً باستخدام `scope={resource}/.default`. أداة التحقق في هذا الدليل مستقلة عن المزود.
- إلمام بـ TypeScript، و async/await، وأساسيات Express.
- عميل متوافق مع MCP للاختبار — واجهة سطر أوامر `@modelcontextprotocol/inspector` هي المسار الأسرع.

لا تحتاج إلى أي من: قاعدة بيانات، خادم OAuth حقيقي (تقبل أداة التحقق عنوان URL لـ JWKs الخاص بأي مزود لديك)، Docker، أو Claude Desktop. يعمل البرنامج التعليمي بالكامل على `tsx`.

## الخطوة 1: تهيئة مشروع TypeScript

أنشئ دليلًا جديدًا وثبّت كل تبعية بالإصدارات المؤكدة في سجل npm في يوم الكتابة[^3]:

```bash
mkdir mcp-tasks-server && cd mcp-tasks-server
npm init -y
npm install \
  @modelcontextprotocol/sdk@1.29.0 \
  zod@4.4.3 \
  express@5.2.1 \
  jose@6.2.3 \
  pino@10.3.1 \
  pino-pretty@13.1.3
npm install --save-dev \
  TypeScript@6.0.3 \
  tsx@4.21.0 \
  '@types/node@^22.19.19' \
  '@types/express@5.0.6'
```

قم بتعديل `package.json` لتمكين ESM وإضافة البرامج النصية:

```json
{
  "name": "mcp-tasks-server",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc -p tsconfig.json",
    "start": "node dist/server.js"
  }
}
```

أنشئ `tsconfig.json` بإعدادات NodeNext الصارمة — حيث يقوم SDK بتعيين عمليات الاستيراد بملحقات `.js` حتى من ملفات TypeScript، لذا فإن `module: NodeNext` إلزامي:

```json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "declaration": false
  },
  "include": ["src/**/*.ts"]
}
```

أنشئ شجرة المصادر:

```bash
mkdir -p src/auth src/tools src/observability
```

## الخطوة 2: تسجيل أدوات ذات أنواع محددة باستخدام McpServer و zod

يوفر SDK فئة `McpServer` عالية المستوى تتعامل مع JSON-RPC، والتفاوض على القدرات، وإدراج الأدوات نيابة عنك. تتلقى معالجات الأدوات وسيطًا ثانيًا يحمل `authInfo` — وهنا يتم فرض النطاق.

أنشئ `src/tools/tasks.ts`:

```ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

// In-memory store keyed by OAuth subject ("sub" claim). A real server would use
// a database; the API surface is identical.
type Task = { id: string; title: string; done: boolean; ownerSub: string };
const tasks: Task[] = [];

function requireScope(authInfo: { scopes?: string[] } | undefined, scope: string) {
  if (!authInfo?.scopes?.includes(scope)) {
    throw new Error(`Forbidden: missing required scope "${scope}"`);
  }
}

export function registerTaskTools(server: McpServer) {
  server.registerTool(
    "list_tasks",
    {
      title: "List tasks",
      description: "List tasks owned by the authenticated user.",
      inputSchema: { includeDone: z.boolean().optional().default(false) },
    },
    async ({ includeDone }, { authInfo }) => {
      requireScope(authInfo, "tasks:read");
      const sub = (authInfo?.extra?.sub as string) ?? "";
      const mine = tasks.filter(
        (t) => t.ownerSub === sub && (includeDone || !t.done),
      );
      return {
        content: [{ type: "text", text: JSON.stringify(mine, null, 2) }],
      };
    },
  );

  server.registerTool(
    "create_task",
    {
      title: "Create task",
      description: "Create a new task owned by the authenticated user.",
      inputSchema: { title: z.string().min(1).max(140) },
    },
    async ({ title }, { authInfo }) => {
      requireScope(authInfo, "tasks:write");
      const sub = (authInfo?.extra?.sub as string) ?? "";
      const task: Task = {
        id: crypto.randomUUID(),
        title,
        done: false,
        ownerSub: sub,
      };
      tasks.push(task);
      return { content: [{ type: "text", text: JSON.stringify(task) }] };
    },
  );

  server.registerTool(
    "complete_task",
    {
      title: "Mark task complete",
      description: "Mark a task complete by id.",
      inputSchema: { id: z.string().uuid() },
    },
    async ({ id }, { authInfo }) => {
      requireScope(authInfo, "tasks:write");
      const sub = (authInfo?.extra?.sub as string) ?? "";
      const task = tasks.find((t) => t.id === id && t.ownerSub === sub);
      if (!task) throw new Error("Task not found");
      task.done = true;
      return { content: [{ type: "text", text: JSON.stringify(task) }] };
    },
  );
}
```

هناك ثلاث تفاصيل مهمة هنا. أولاً، `inputSchema` هو كائن بسيط من مخططات zod — يقوم SDK بتكييف كل منها مع واجهة المخطط القياسي (Standard Schema) ويقوم بإنشاء JSON Schema لبروتوكول النقل[^4]. ثانياً، الوسيط الثاني لكل معالج هو `{ authInfo, ... }`، والذي يتم ملؤه بواسطة برمجية bearer-auth الوسيطة في الخطوة 5؛ نحن لا نثق أبداً في وسيط `sub` قادم من العميل. ثالثاً، فحص `requireScope` داخل المعالج مقصود: حتى عندما يقوم خادم التفويض (AS) بإصدار رمز بالنطاقات الصحيحة، فإنه لا يعرف الأداة التي يوشك العميل على استدعائها، لذا فإن فرض النطاق هو مسؤولية الخادم[^5].

## الخطوة 3: ربط Streamable HTTP transport خلف Express

حل Streamable HTTP محل SSE transport المهجور في مراجعة المواصفات لشهر مارس 2025[^6]. يعمل عبر نقطة نهاية واحدة (سنستخدم `/mcp`) ويستخدم ثلاثة أفعال (verbs): `POST` لـ JSON-RPC من العميل إلى الخادم، و `GET` لفتح تدفق SSE للإشعارات من الخادم إلى العميل، و `DELETE` لإنهاء الجلسة.

أنشئ `src/server.ts`:

```ts
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { registerTaskTools } from "./tools/tasks.js";
import { logger, requestIdMiddleware } from "./observability/logger.js";
import { protectedResourceMetadata } from "./auth/prm.js";
import { requireBearerToken } from "./auth/verifier.js";

const PORT = Number(process.env.PORT ?? 3333);
const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL ?? `http://localhost:${PORT}`;

const app = express();
app.use(express.json({ limit: "1mb" }));
app.use(requestIdMiddleware);

// RFC 9728 — Protected Resource Metadata. Anonymous, must be reachable.
app.get("/.well-known/oauth-protected-resource", protectedResourceMetadata);

// Map of sessionId → transport. Streamable HTTP is stateful by default.
const transports = new Map<string, StreamableHTTPServerTransport>();

app.all("/mcp", requireBearerToken, async (req, res) => {
const sessionHeader = req.header("mcp-session-id");
let transport = sessionHeader ? transports.get(sessionHeader) : undefined;

if (!transport && req.method === "POST" && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => transports.set(sid, transport!),
enableDnsRebindingProtection: true,
allowedHosts: [`localhost:${PORT}`, `127.0.0.1:${PORT}`],
allowedOrigins: ["https://claude.ai", "https://app.cursor.com"],
});
transport.onclose = () => {
if (transport!.sessionId) transports.delete(transport!.sessionId);
};
const server = new McpServer({ name: "tasks", version: "0.1.0" });
registerTaskTools(server);
await server.connect(transport);
}

if (!transport) {
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "No active session for this request" },
id: null,
});
return;
}

await transport.handleRequest(req, res, req.body);
});

app.listen(PORT, () => {
logger.info({ port: PORT, baseUrl: PUBLIC_BASE_URL }, "mcp server listening");
});
```

هناك بعض النقاط التي تستحق القراءة بعناية. خريطة الجلسة موجودة في الذاكرة؛ إذا قمت بالتوسع أفقياً، فانتقل إلى `sessionIdGenerator: undefined` للوضع عديم الحالة (stateless) واقبل فقدان الإشعارات من الخادم إلى العميل. `enableDnsRebindingProtection: true` هي ميزة الأمان التي أضافتها Anthropic في SDK 1.24.0[^7] — فهي ترفض أي طلب لا يوجد رأس `Host` الخاص به في `allowedHosts`، مما يحبط هجوم rebinding الذي وثقه Straiker في أواخر عام 2025. ضع اسم المضيف الحقيقي للإنتاج في `allowedHosts` وأصول عملاء MCP الموثوق بهم في `allowedOrigins`.

## الخطوة 4: نشر البيانات الوصفية للموارد المحمية وفقًا لـ RFC 9728

تكتشف عملاء MCP مكان الحصول على الرمز من خلال جلب مستند البيانات الوصفية للموارد المحمية الخاص بالخادم. الموقع ثابت: `/.well-known/oauth-protected-resource`[^8]. جعلت مراجعة المواصفات في نوفمبر 2025 نشر هذا المستند إلزامياً لخوادم MCP التي تستخدم HTTP-transport[^9].

أنشئ `src/auth/prm.ts`:

```ts
import type { Request, Response } from "express";

const PUBLIC_BASE_URL =
process.env.PUBLIC_BASE_URL ?? "http://localhost:3333";
const AUTH_SERVER = process.env.OAUTH_ISSUER!; // e.g. https://example.auth0.com/

export function protectedResourceMetadata(_req: Request, res: Response) {
res.json({
resource: `${PUBLIC_BASE_URL}/mcp`,
authorization_servers: [AUTH_SERVER],
bearer_methods_supported: ["header"],
scopes_supported: ["tasks:read", "tasks:write"],
resource_documentation: `${PUBLIC_BASE_URL}/docs`,
});
}
```

هناك حقلان يقومان بالدور الأهم. قيمة `resource` هي عنوان URL الدقيق الذي يجب على العملاء ترديده كمعلمة `resource` عند طلب رمز؛ إذا اختلف هذان السلسلتان، فسيفشل التحقق من صحة الجمهور RFC 8707 (بشكل صحيح) في وقت التحقق. تسرد مصفوفة `authorization_servers` كل خادم تفويض يثق به المورد — يختار العملاء واحداً، ويجلبون مستند `/.well-known/oauth-authorization-server` الخاص به (RFC 8414[^10])، ويقومون بتشغيل تدفق كود التفويض القياسي + PKCE. لا يقوم خادم MCP نفسه بإصدار الرموز أبداً.

إذا كنت بحاجة إلى عملاء لا يدعمون التسجيل الديناميكي للعملاء (RFC 7591[^11])، فقد أضافت مواصفات نوفمبر 2025 مسار تسجيل ثانٍ يسمى "وثائق بيانات تعريف معرف العميل" (Client ID Metadata Documents) — حيث يعرّف العملاء أنفسهم باستخدام رابط URL يتحكمون فيه[^12]. يعتبر كل من DCR و CIMD اختياريين من وجهة نظر خادم الموارد؛ عليك فقط دعم أيهما يدعمه خادم الـ AS الخاص بك.

## الخطوة 5: التحقق من رموز الوصول باستخدام jose وفرض مؤشرات الموارد

أداة التحقق صغيرة ولكن كل سطر فيها له أهميته. فهي تتحقق من توقيع JWT مقابل JWKS الخاص بـ AS، وتتحقق من صحة `iss`، وتتحقق من صحة `aud` مقابل رابط URL للمورد (هذا هو تطبيق RFC 8707[^13])، وتتحقق من `exp`، وأخيرًا تشتق مصفوفة `scopes` من ادعاء `scope`.

أنشئ `src/auth/verifier.ts`:

```ts
import { createRemoteJWKSet, jwtVerify } from "jose";
import type { NextFunction, Request, Response } from "express";

const OAUTH_ISSUER = process.env.OAUTH_ISSUER!;
const PUBLIC_BASE_URL =
  process.env.PUBLIC_BASE_URL ?? "http://localhost:3333";
const RESOURCE_URL = `${PUBLIC_BASE_URL/mcp`;
const JWKS = createRemoteJWKSet(new URL(`${OAUTH_ISSUER.well-known/jwks.json`));

export interface AuthInfo {
  token: string;
  clientId?: string;
  scopes: string[];
  expiresAt?: number;
  extra: { sub?: string; iss?: string };
}

declare module "express-serve-static-core" {
  interface Request {
    auth?: AuthInfo;
  }
}

export async function requireBearerToken(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const header = req.header("authorization") ?? "";
  if (!header.toLowerCase().startsWith("bearer ")) {
    return unauthorized(res, "missing_bearer_token");
  }
  const token = header.slice("bearer ".length);

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: OAUTH_ISSUER,
      audience: RESOURCE_URL,
    });
    const scopes =
      typeof payload.scope === "string" ? payload.scope.split(" ") : [];
    req.auth = {
      token,
      clientId:
        typeof payload.azp === "string"
          ? payload.azp
          : typeof payload.client_id === "string"
            ? payload.client_id
            : undefined,
      scopes,
      expiresAt: typeof payload.exp === "number" ? payload.exp : undefined,
      extra: {
        sub: typeof payload.sub === "string" ? payload.sub : undefined,
        iss: typeof payload.iss === "string" ? payload.iss : undefined,
      },
    };
    next();
  } catch (err) {
    return unauthorized(res, "invalid_token", (err as Error).message);
  }
}

function unauthorized(res: Response, code: string, description?: string) {
  // RFC 9728 requires the WWW-Authenticate challenge to point at the PRM.
  res
    .status(401)
    .set(
      "WWW-Authenticate",
      `Bearer realm="mcp", error="${code", resource_metadata="${process.env.PUBLIC_BASE_URL ?? "http://localhost:3333"/.well-known/oauth-protected-resource"${description ? `, error_description="${description"` : ""`,
    )
    .json({ error: code, error_description: description });
}
```

يقوم البرنامج الوسيط (middleware) بكتابة `AuthInfo` المكتشفة في `req.auth`، والتي يقرأها `StreamableHTTPServerTransport.handleRequest` ويمررها إلى كل معالج أداة كمعامل `authInfo` كما رأينا في الخطوة 2. يتضمن ترويسة `WWW-Authenticate` معامل `resource_metadata` حتى يتمكن العميل الذكي من اكتشاف مكان الحصول على رمز مميز تلقائيًا بعد خطأ 401 — وهذا هو التسهيل الذي كُتب RFC 9728 لتمكينه[^14].

## الخطوة 6: فرض التفويض القائم على النطاق داخل معالجات الأدوات

تعتبر الأداة المساعدة `requireScope` من الخطوة 2 هي النصف الثاني من القصة. تتدفق النطاقات (Scopes) من AS إلى JWT، عبر أداة التحقق، إلى `req.auth.scopes`، وأخيرًا إلى `authInfo.scopes` الخاص بمعالج الأداة. التحقق داخل المعالج مقصود: يمكن لطبقة الشبكة المصادقة، ولكن الأداة نفسها هي الوحيدة التي تعرف النطاق الذي تحتاجه.

عند ربط هذا بـ AS حقيقي، قم بإنشاء نطاقين — `tasks:read` و `tasks:write` — وتأكد من أن طلب الرمز المميز الخاص بك يتضمن `scope=tasks:read tasks:write` و (في المزودين الذين يدعمون ذلك) `resource=https://your-host/mcp`. يدعم WorkOS AuthKit معيار RFC 8707 بشكل أصلي[^15]؛ ويدعمه Auth0 بمجرد تمكين ملف تعريف توافق معامل المورد (Resource Parameter Compatibility Profile)[^16]؛ تحقق من وثائق المزود الخاص بك قبل الافتراض.

هناك نمطان للإنتاج يستحقان الاعتماد الآن بينما الكود لا يزال حديثًا. أولاً، لا تقرأ أبدًا `sub` من معاملات JSON-RPC — اسحبها دائمًا من `authInfo.extra.sub`. تتبع الأدوات المثال هذه القاعدة، ولهذا السبب تقوم بتخزين المهام مفهرسة بمعرف الموضوع (subject) الذي تم التحقق منه وليس بأي شيء قدمه العميل. ثانيًا، يجب أن تكون عمليات التحقق من النطاق دائمًا السطر الأول في المعالج بحيث تكون استجابات الخطأ موحدة؛ إذا قمت بنشرها لاحقًا، فسوف تنسى واحدة في النهاية.

## الخطوة 7: تفعيل الحماية من إعادة ربط DNS والقائمة البيضاء للمضيفين

هجوم إعادة ربط DNS (DNS-rebinding) الذي أبلغ عنه Straiker في أواخر عام 2025 استغل أي خادم MCP يستمع على localhost: حيث دفع موقع ويب ضار المتصفح لتحليل اسم مضيف تحت سيطرته إلى `127.0.0.1` وقام بتشغيل الأدوات ضد الخادم المحلي بهوية الشبكة الخاصة بالمستخدم الزائر[^17]. أطلقت Anthropic الحل في `@modelcontextprotocol/sdk` 1.24.0 — وهو التحقق من ترويسة Host الذي يرفض أي طلب لا توجد ترويسة `Host` الخاصة به في قائمة `allowedHosts` المهيأة. الحماية اختيارية (معطلة افتراضيًا للتوافق مع الإصدارات السابقة، وفقًا للتوصية CVE-2025-66414) ما لم تستخدم المساعد `createMcpExpressApp()`، لذا قمنا بضبطها صراحةً على وسيلة النقل (transport).

خيارات `StreamableHTTPServerTransport` المستخدمة في الخطوة 3 تمكن الحماية بالفعل:

```ts
new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  onsessioninitialized: (sid) => transports.set(sid, transport!),
  enableDnsRebindingProtection: true,
  allowedHosts: [`localhost:${PORT`, `127.0.0.1:${PORT`],
  allowedOrigins: ["https://claude.ai", "https://app.cursor.com"],
});
```

بالنسبة لنشر الإنتاج، استبدل مدخلات localhost باسم المضيف الحقيقي الخاص بك وأضف كل أصل (origin) لعميل MCP تنوي دعمه إلى `allowedOrigins`. تقوم كلتا القائمتين بمطابقة سلاسل نصية صارمة — الرموز البديلة (wildcards) غير مدعومة عمدًا. توصي وثيقة أفضل ممارسات أمان MCP صراحةً بتهيئة كلتا القائمتين في الإنتاج[^18].

هناك وسيلة تقوية ثانية تستحق الإضافة إلى نفس تطبيق Express: تحديد معدل الطلبات (rate limiting). يعتمد SDK بالفعل على `express-rate-limit`؛ أبسط إعداد للإنتاج هو تثبيته على `/mcp` بحد أقصى 60 طلبًا في الدقيقة لكل عنوان IP للطلبات غير المصادق عليها وحد أعلى للطلبات المصادق عليها.

## الخطوة 8: القابلية للملاحظة المنظمة باستخدام pino ومعرفات الطلبات

تفشل استدعاءات الأدوات بطرق غامضة عندما لا توجد سجلات. الحد الأدنى من النمط المفيد هو برنامج وسيط لمعرف الطلب (request-id) يربط معرفًا بكل طلب ومثيل pino يصدر سطرًا واحدًا منظمًا لكل استدعاء أداة.

أنشئ `src/observability/logger.ts`:

```ts
import pino from "pino";
import { randomUUID } from "node:crypto";
import type { NextFunction, Request, Response } from "express";

export const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  transport:
    process.env.NODE_ENV === "production"
      ? undefined
      : { target: "pino-pretty", options: { translateTime: "SYS:HH:MM:ss.l" } },
  base: { service: "mcp-tasks-server" },
});

declare module "express-serve-static-core" {
  interface Request {
    log?: pino.Logger;
    requestId?: string;
  }
}

export function requestIdMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const requestId =
    req.header("x-request-id") ?? randomUUID();
  req.requestId = requestId;
  req.log = logger.child({ requestId });
  res.setHeader("x-request-id", requestId);

const started = process.hrtime.bigint();
  res.on("finish", () => {
    const latencyMs = Number(process.hrtime.bigint() - started) / 1_000_000;
    req.log!.info(
      {
        method: req.method,
        path: req.path,
        status: res.statusCode,
        latencyMs: Math.round(latencyMs * 100) / 100,
        clientId: req.auth?.clientId,
        userSub: req.auth?.extra?.sub,
      },
      "request",
    );
  });
  next();
}
```

عندما يكون هذا في مكانه، يصدر كل طلب سطر سجل JSON واحدًا في الإنتاج وسطرًا ملونًا في التطوير. يمرر كل من MCP Inspector و Claude Desktop ترويسة `x-request-id` التي قمت بتعيينها في الاستجابة، لذا يعمل الارتباط من البداية إلى النهاية دون الحاجة إلى المزيد من التوصيلات.

## التحقق

قم بتشغيل الخادم باستخدام رابط URL حقيقي للمصدر (issuer). يستخدم البرنامج التعليمي `auth.example.com` كبديل — استبدله بـ AS الخاص بك:

```bash
OAUTH_ISSUER=https://auth.example.com/ \
PUBLIC_BASE_URL=http://localhost:3333 \
npm run dev
```

في نافذة أوامر ثانية، تأكد من إمكانية الوصول إلى وثيقة بيانات تعريف المورد المحمي:

```bash
curl -s http://localhost:3333/.well-known/oauth-protected-resource | jq .
```

المخرجات المتوقعة:

```json
{
  "resource": "http://localhost:3333/mcp",
  "authorization_servers": ["https://auth.example.com/"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["tasks:read", "tasks:write"],
  "resource_documentation": "http://localhost:3333/docs"
}
```

تأكد من أن الطلبات المجهولة تعيد خطأ 401 مع تحدي `WWW-Authenticate` يشير مرة أخرى إلى PRM:

```bash
curl -i -X POST http://localhost:3333/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'
```

يجب أن ترى:

```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp", error="missing_bearer_token", resource_metadata="http://localhost:3333/.well-known/oauth-protected-resource"
```

لتجربة المسار الناجح بالكامل، قم بتشغيل Inspector الرسمي ضد الخادم. اكتب ملف تكوين يصف الخادم البعيد:

```json
// mcp.json
{
  "mcpServers": {
    "tasks": {
      "type": "streamable-http",
      "url": "http://localhost:3333/mcp"
    }
  }
}
```

ثم قم بتشغيل Inspector مشيرًا إلى ذلك التكوين:

```bash
npx @modelcontextprotocol/inspector --config mcp.json --server tasks
```

يبدأ Inspector واجهة مستخدم ويب محلية على `http://localhost:6274`. افتحها، والصق رمز الحامل (bearer token) الخاص بك في حقل ترويسة "Authorization" في لوحة الاتصال، وانقر على "Connect". بمجرد الاتصال، تسرد واجهة المستخدم الأدوات الثلاث وتسمح لك باستدعاء `create_task` و `list_tasks` و `complete_task` ضد الخادم المباشر. راقب سجلات pino الخاصة بك — كل مكالمة تصدر سطرًا منظمًا واحدًا يحمل `clientId` و `userSub` و `latencyMs`.

إذا كنت تفضل البرمجة النصية، فإن نفس Inspector يوفر وضع `--cli` لخوادم stdio؛ اختبار HTTP عن بُعد هو الأسهل من خلال واجهة المستخدم، أو يمكنك تشغيل نقطة نهاية JSON-RPC مباشرة باستخدام `curl` بمجرد حصولك على معرف جلسة من استجابة التهيئة (initialize).

## الأخطاء الشائعة

هناك بعض الأشياء التي قد تواجه المطورين لأول مرة. كل واحدة من هذه المشاكل واجهتني أو واجهت شخصًا أعمل معه في بناء MCP حقيقي في عام 2026.

**`Error: Invalid Host header`** — العميل الخاص بك يرسل قيمة `Host` ليست موجودة في `allowedHosts`. إما أن تضيف المضيف إلى القائمة أو، إذا كنت تعمل خلف موازن تحميل (load balancer) يقوم بإعادة كتابة الـ Host، فقم بتعيين `allowedHosts` على القيمة المرئية خارجيًا. لا تقم بتعطيل `enableDnsRebindingProtection`؛ حيث تدرج مواصفات نوفمبر 2025 التخفيف من إعادة الربط (rebinding mitigation) كـ SHOULD[^19].

**`Error: audience claim check failed`** — خيار `audience` الخاص بـ `jose` يقارن مقابل ادعاء (claim) الـ `aud` في JWT. تتطلب مواصفات MCP أن يكون `aud` هو عنوان URL للمورد الذي أصدر الـ AS الرمز (token) من أجله. تأكد من ثلاثة أشياء: (1) أن عميلك قام بتضمين `resource=http://localhost:3333/mcp` في طلب الرمز، (2) أن الـ AS الخاص بك يدعم RFC 8707، و (3) أن القيمة في أداة التحقق (verifier) الخاصة بك تطابق حقل `resource` في مستند PRM حرفًا بحرف.

**`Cannot find module ... .js`** — يتطلب TypeScript مع `module: NodeNext` وجود امتدادات `.js` في عمليات الاستيراد النسبية حتى من ملفات `.ts`. هذه ضريبة تعلم لمرة واحدة؛ بمجرد تكوين الإكمال التلقائي في المحرر الخاص بك، ستختفي المشكلة.

**قائمة الأدوات تعود فارغة** — غالبًا ما تكون مشكلة في الجلسة (session). تأكد من أن استجابة طلب `POST /mcp` الأولي تتضمن ترويسة `mcp-session-id` وأن عميلك يقوم بإرسالها في الطلبات اللاحقة. يقوم الـ Inspector بذلك تلقائيًا؛ بينما لا يفعل curl ذلك.

**المعالج (handler) يرمي خطأ "Forbidden: missing required scope"** — أصدر الـ AS رمزًا بدون الصلاحيات (scopes) التي طلبتها. كل من Auth0 و Entra يقيدان الصلاحيات خلف موافقة صريحة وتسجيل API؛ تأكد مرة أخرى من تسجيل الصلاحية على مورد API وأن تطبيق العميل مسموح له بطلبها.

**`MODULE_NOT_FOUND` لـ `@modelcontextprotocol/sdk/server/auth/...`** — شائع عند نسخ ولصق مقتطفات برمجية قديمة. يجب أن تنتهي عمليات الاستيراد بـ `.js`، وليس `.ts` أو بدون امتداد. توفر الـ SDK نقطة دخول ESM ويفرض Node بدقة الامتدادات من أجل دقة `NodeNext`.

## الخطوات التالية

لديك الآن خادم MCP قابل للتشغيل يمكن لعميل MCP المصادقة عليه، ويفرض مؤشرات موارد RFC 8707، وينشر البيانات الميتا للموارد المحمية RFC 9728، ويرفض محاولات إعادة ربط DNS عبر القائمة البيضاء لترويسة Host، ويصدر سجلات منظمة يمكنك توجيهها إلى أي نظام مراقبة (observability stack).

إليك بعض المتابعات الطبيعية:

- استبدل مصفوفة `tasks` الموجودة في الذاكرة بقاعدة بيانات حقيقية. إذا كنت تريد مثالاً عمليًا لأنماط Postgres التي تحافظ على حالة الجلسة والتي تعمل بشكل جيد مع Streamable HTTP، فإن [درس التواجد في الوقت الفعلي باستخدام Postgres LISTEN/NOTIFY](https://nerdleveltech.com/postgres-listen-notify-realtime-presence-tutorial) يغطي دورة حياة الاتصال طويل الأمد التي تريدها هنا.
- ادفع الخادم إلى الحافة (edge). يشرح [درس Cloudflare Workers R2 image CDN](https://nerdleveltech.com/cloudflare-workers-r2-image-cdn-tutorial) أساسيات Workers التي تترجم تقريبًا حرفيًا لتشغيل خادم MCP على مسار Workers — حيث يقبل `StreamableHTTPServerTransport` الخاص بالـ SDK بالفعل وضعًا عديم الحالة (stateless) للعمليات بدون خادم (serverless).
- قم بتشغيل الخادم خلف خلفية Postgres مجمعة (pooled) عندما تنتقل من `Map<string, transport>` إلى نشر متعدد النسخ (multi-instance). راجع [تجميع Postgres للإنتاج باستخدام PgBouncer و Supavisor](https://nerdleveltech.com/production-postgres-pooling-pgbouncer-Supabase-supavisor-tutorial) لمعرفة أنماط تجميع الاتصالات التي تتحمل التوسع الأفقي.
- للحصول على خلفية مفاهيمية حول ماهية MCP وموقعه في بنية الوكيل (agent stack)، اقرأ نظرة عامة على [شرح خوادم MCP](https://nerdleveltech.com/mcp-servers-explained-claudes-new-ai-backbone-for-real-automation).

بمجرد تشغيل الخادم، فإن الشيء التالي الذي تريده كل الفرق هو عميل MCP مخصص يمكنه استدعاؤه برمجياً؛ هذا دليل ليوم آخر، لكن تدفق المصادقة الذي بنيته للتو هو بالضبط ما سيستهلكه العميل في الطرف الآخر.

[^1]: Model Context Protocol Authorization specification (current draft tracking the November 25, 2025 revision). <https://modelcontextprotocol.io/specification/draft/basic/authorization>

[^2]: Node.js release schedule — Node 24 is the current Active LTS through October 2026; Node 22 is in Maintenance LTS through April 2027. <https://nodejs.org/en/about/previous-releases>

[^3]: `@modelcontextprotocol/sdk` 1.29.0 on the npm registry, verified May 14, 2026. <https://www.npmjs.com/package/@modelcontextprotocol/sdk>

[^4]: MCP TypeScript SDK server documentation — tool registration with Standard Schema input. <https://GitHub.com/modelcontextprotocol/TypeScript-sdk/blob/main/docs/server.md>

[^5]: MCP Authorization spec — clients SHOULD request the minimum scope; servers MUST enforce. <https://modelcontextprotocol.io/specification/draft/basic/authorization>

[^6]: MCP transports specification — Streamable HTTP replaces SSE (2025-03-26). <https://modelcontextprotocol.io/specification/2025-03-26/basic/transports>

[^7]: `@modelcontextprotocol/sdk` 1.24.0 release notes — DNS rebinding protection added. <https://GitHub.com/modelcontextprotocol/TypeScript-sdk/releases>

[^8]: RFC 9728 — OAuth 2.0 Protected Resource Metadata. <https://datatracker.ietf.org/doc/html/rfc9728>

[^9]: Auth0 — MCP specs update covering the June 18, 2025 and November 25, 2025 authorization revisions. <https://auth0.com/blog/mcp-specs-update-all-about-auth/>

[^10]: RFC 8414 — OAuth 2.0 Authorization Server Metadata. <https://datatracker.ietf.org/doc/html/rfc8414>

[^11]: RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol. <https://datatracker.ietf.org/doc/html/rfc7591>

[^12]: آرون باريكي — تسجيل العميل وإدارة المؤسسات في مواصفة تفويض MCP لشهر نوفمبر 2025. <https://aaronparecki.com/2025/11/25/1/mcp-authorization-spec-update>

[^13]: RFC 8707 — مؤشرات الموارد لبروتوكول OAuth 2.0. <https://datatracker.ietf.org/doc/html/rfc8707>

[^14]: دين ديليمارسكي — ما الجديد في مواصفة تفويض MCP بتاريخ 25-11-2025. <https://den.dev/blog/mcp-november-authorization-spec/>

[^15]: WorkOS — مؤشرات الموارد لتفويض MCP (AuthKit). <https://workos.com/changelog/resource-indicators-for-mcp-auth>

[^16]: Auth0 — ملف تعريف توافق بارامتر الموارد لـ MCP. <https://auth0.com/ai/docs/mcp/guides/resource-param-compatibility-profile>

[^17]: Straiker — خطر الوكلاء: إعادة ربط DNS يكشف خوادم MCP الداخلية. <https://www.straiker.ai/blog/agentic-danger-dns-rebinding-exposing-your-internal-mcp-servers>

[^18]: MCP — أفضل ممارسات الأمان. <https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices>

[^19]: مواصفة تفويض MCP — تخفيف مخاطر إعادة ربط DNS والتحقق من Host-header. <https://modelcontextprotocol.io/specification/draft/basic/authorization>
