ai-ml

فحص جمهور MCP Token: تم اختبار expectedResource (2026)

٦ أكتوبر ٢٠٢٦

MCP Token Audience Check: expectedResource Tested (2026)

ملخص: التحقق من جمهور توكن MCP هو التأكد من أن توكن الوصول قد تم إصداره لخادم MCP الخاص بك وليس لخدمة أخرى. أضافت SDK الخاصة بـ TypeScript خياراً لذلك في 2 أكتوبر 2026، وهو معطل حتى تقوم بتفعيله.1

في اختباراتي على SDK 1.32.1، حصل توكن تم إصداره لخادم مختلف على استجابة HTTP 200 حتى قمت بتعيين expectedResource، وبعدها حصل على HTTP 401.2 أما في @modelcontextprotocol/express 2.0.1، والمثبت بجانب server 2.3.1، فإن الخيار لم يفعل شيئاً على الإطلاق.2

التحقق من جمهور توكن MCP: الإجابة المختصرة

يعمل خادم MCP المحمي كخادم موارد OAuth. تنص المواصفات على أنه يجب ألا يقبل إلا التوكنات الصادرة له، لذا لا يمكن إعادة استخدام توكن تم إنشاؤه لـ API آخر ضده.3

في SDK الخاصة بـ TypeScript، تقوم requireBearerAuth بهذا التحقق من خلال expectedResource. تقوم بتعيينه على القيمة التي يضعها خادم التفويض الخاص بك في التوكنات المخصصة لخادمك، ويجب أن يبلغ الموثق (verifier) الخاص بك عن هذه القيمة كـ AuthInfo.resource.4 في أنواع SDK، كلاهما عبارة عن كائنات URL.52

ما ستتعلمه

  • ما تتطلبه مواصفات MCP من الخادم بخصوص جمهور التوكن
  • ما الذي تغير في SDK الخاصة بـ TypeScript في 2 أكتوبر 2026
  • نظام اختبار صغير يختبر عشرة أشكال من التوكنات مقابل الحماية
  • كيف تقوم expectedResource بمقارنة القيم، بما في ذلك الشرطات المائلة (slashes)، والأجزاء (fragments)، وحالة الأحرف
  • لماذا يقوم الموثق الذي ينسى resource بحظر جميع المستخدمين
  • لماذا يهم إصدار محول Express
  • إعداد جاهز للنسخ واللصق وقائمة مراجعة

ما تقوله مواصفات MCP عن جمهور التوكن

تقول صفحة التفويض بتاريخ 2026-07-28 أن الخوادم "يجب أن تتحقق من أن توكنات الوصول قد صدرت خصيصاً لها كجمهور مستهدف"، وفقاً لـ RFC 8707 القسم 2.3 وتدرج صفحة إصدارات MCP تاريخ 2026-07-28 كمراجعة حالية.6

تكرر صفحة الأمان ذلك كقاعدة حول "تقييد امتيازات توكن الوصول": يجب على الخوادم رفض التوكنات التي لا تتضمنها في ادعاء الجمهور (audience claim)، أو التحقق بطريقة أخرى من أنها المستلم المستهدف.3

تحدد RFC 8707، التي نُشرت في فبراير 2020، معامل طلب resource الذي يسمح للعميل بإخبار خادم التفويض عن المورد الذي يريد توكناً له.7

كما تمنع المواصفات التمرير المباشر (passthrough). إذا كان خادم MCP الخاص بك يتصل بـ API علوي، تقول صفحة الأمان أنه "يجب ألا يمرر التوكن الذي استلمه من عميل MCP".3

ما الذي تغير في TypeScript SDK في 2 أكتوبر

أصدر خط v1 الإصدار @modelcontextprotocol/sdk 1.32.0 في 2 أكتوبر 2026. وأصدر خط v2 الإصدار @modelcontextprotocol/server 2.3.0 في نفس اليوم.1

تذكر ملاحظات إصدار 1.32.0 خيارين جديدين، "كلاهما معطل ما لم تقم بتعيينهما". أحدهما هو maxToolInputElements. والآخر هو expectedResource في requireBearerAuth، والذي "يقبل فقط الرموز (tokens) الصادرة لهذا الخادم (جمهور الرمز - token's audience)". وتذكر ملاحظات 2.3.0 نفس الخيارين كتغييرات بسيطة، وتقول إنه في حالة عدم تعيين expectedResource، فلن يتغير شيء.1

تم دمج طلب السحب (pull request) الذي أضاف هذه الميزة إلى v2 في 2 أكتوبر، ويذكر أن المقارنة تتم قبل فحوصات النطاق والصلاحية.5 ويتفق ملف bearerAuth.js في حزمة 1.32.1 المثبتة مع ذلك: فحص الجمهور يأتي أولاً.2

في 5 أكتوبر، أصدر المشروع الإصدارين 1.32.1 و 2.3.1. وتضيف ملاحظات 2.3.1 نفس الخيار إلى @modelcontextprotocol/server-legacy.1

بناء منصة اختبار لجمهور الرموز (token audience)

لم أرغب في الاعتماد على سجل التغييرات، لذا قمت ببناء خادم تفويض وهمي وحارس (guard). يحتوي الخادم على عشرة رموز، كل منها بشكل جمهور مختلف، بما في ذلك رمز لا يحتوي على مطالبة aud على الإطلاق.2

يختار المتحقق إدخال aud الخاص بمضيف هذا الخادم ويعيده كـ resource. وهذا يحاكي النمط الموجود في وثائق SDK، والتي تذكر "إدخال هذا الخادم" من aud وتترك resource غير معين عند عدم وجوده.4

// tokens.mjs: a fake authorization server. Opaque token -> claims (stands in for a JWT's `aud`).
export const SERVER_URL = new URL('https://mcp.example.com/mcp');
const exp = Math.floor(Date.now() / 1000) + 3600;

export const CLAIMS = {
  'tok-ours':      { aud: 'https://mcp.example.com/mcp', exp },
  'tok-other':     { aud: 'https://other.example.org/mcp', exp },   // issued for a different server
  'tok-no-aud':    { exp },                                          // no audience at all
  'tok-trailing':  { aud: 'https://mcp.example.com/mcp/', exp },     // one trailing slash
  'tok-fragment':  { aud: 'https://mcp.example.com/mcp#x', exp },    // fragment
  'tok-two-slash': { aud: 'https://mcp.example.com/mcp//', exp },    // two trailing slashes
  'tok-origin':    { aud: 'https://mcp.example.com', exp },          // origin only, no path
  'tok-http':      { aud: 'http://mcp.example.com/mcp', exp },       // wrong scheme
  'tok-upper':     { aud: 'HTTPS://MCP.EXAMPLE.COM/mcp', exp },      // uppercase scheme and host
  'tok-list':      { aud: ['https://other.example.org/mcp', 'https://mcp.example.com/mcp'], exp }, // array
};

// mode: 'url' reports `resource` as a URL object, which the SDK types require and its docs use.
// 'string' reports a plain string: only plain JavaScript allows that. 'none' reports no `resource`.
export function makeVerifier({ mode = 'string' } = {}) {
  return {
    async verifyAccessToken(token) {
      const c = CLAIMS[token];
      if (!c) throw new Error('unknown token');
      const auds = [c.aud ?? []].flat();
      const hit = auds.find(a => { try { return new URL(a).host === 'mcp.example.com'; } catch { return false; } });
      const info = { token, clientId: 'client-1', scopes: ['mcp'], expiresAt: c.exp };
      if (mode === 'string' && hit) info.resource = hit;
      if (mode === 'url' && hit) info.resource = new URL(hit);
      return info;
    },
  };
}

يبدأ الملف الثاني تشغيل تطبيقين Express، أحدهما بدون الخيار والآخر به. ويرسل كل رمز إلى كل منهما، بالإضافة إلى طلب واحد بدون ترويسة Authorization، ويطبع أكواد الحالة.

// matrix.mjs: node matrix.mjs string | node matrix.mjs url | node matrix.mjs none | node matrix.mjs url origin
import express from 'express';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { CLAIMS, SERVER_URL, makeVerifier } from './tokens.mjs';

const mode = process.argv[2] ?? 'string';
const expected = process.argv[3] === 'origin' ? new URL('https://mcp.example.com') : SERVER_URL;

async function statuses(expectedResource) {
  const app = express();
  const guard = requireBearerAuth({ verifier: makeVerifier({ mode }), requiredScopes: ['mcp'], expectedResource });
  app.post('/mcp', guard, (req, res) => res.json({ ok: true }));
  const server = await new Promise(r => { const s = app.listen(0, '127.0.0.1', () => r(s)); });
  const url = `http://127.0.0.1:${server.address().port}/mcp`;
  const out = {};
  for (const t of Object.keys(CLAIMS)) {
    out[t] = (await fetch(url, { method: 'POST', headers: { authorization: `Bearer ${t}` } })).status;
  }
  out['(no header)'] = (await fetch(url, { method: 'POST' })).status;
  server.close();
  return out;
}

const off = await statuses(undefined);
const on = await statuses(expected);
console.log('token'.padEnd(14), 'no option', 'expectedResource');
for (const t of Object.keys(off)) console.log(t.padEnd(14), String(off[t]).padEnd(9), on[t]);

قم بتثبيته وتشغيله في مجلد فارغ مع تعيين "type": "module" في ملف package.json:

npm i @modelcontextprotocol/sdk@1.32.1 express@5
node matrix.mjs string

النتائج: الرمز المخصص لخادم آخر يحصل على HTTP 200 حتى تقوم بتفعيل الخيار

هذا هو المخرج على SDK 1.32.1 مع متحقق يعيد resource كسلسلة نصية، وهو ما يسمح به JavaScript البسيط فقط:2

token          no option expectedResource
tok-ours       200       200
tok-other      200       401
tok-no-aud     200       401
tok-trailing   200       200
tok-fragment   200       200
tok-two-slash  200       401
tok-origin     200       401
tok-http       200       401
tok-upper      200       401
tok-list       200       200
(no header)    401       401

بدون الخيار، حصلت جميع الرموز العشرة على 200، بما في ذلك tok-other و tok-no-aud. ومع تفعيله، تم رفض ستة رموز.2 في الحارس الخاص بـ SDK، لا يتم التحقق من جمهور رمز MCP حتى تقوم بتعيين expectedResource.

السطر الأخير يرسل طلباً بدون ترويسة Authorization على الإطلاق. وقد حصل على 401 في كلا العمودين، لذا لا يزال الحارس يطلب رمزاً.2

مصفوفة من أكواد حالة HTTP لعشرة أشكال من التوكنات وطلب واحد بدون ترويسة Authorization، عبر ستة تكوينات. SDK 1.31.0 مع تفعيل الخيار و SDK 1.32.1 بدونه يقبلان توكن لخادم آخر وتوكن بدون audience. Express adapter 2.0.1 يفعل الشيء نفسه. SDK 1.32.1 و Express adapter 2.0.2 مع تفعيل الخيار يرفضان كليهما. الشكل 1: أكواد الحالة من تجاربي. الخلايا الحمراء هي توكنات لخادم آخر، أو بدون audience، تم قبولها. الأعمدة من 1 إلى 3 استخدمت verifier يعيد resource كسلسلة نصية (string)، والأعمدة من 4 إلى 6 استخدمت كائن URL.

requireBearerAuth expectedResource: ما يقبله وما يرفضه

يقوم الـ SDK بمقارنة expectedResource مع الـ resource الذي يبلغ عنه الـ verifier الخاص بك كسلاسل نصية، مع تجاهل الـ fragment وشرطة مائلة واحدة في النهاية.15 ونتائجي تتوافق مع ذلك.

إليك ما أظهرته التوكنات العشرة:

  • تطابق تام، شرطة مائلة واحدة في النهاية، fragment: تم القبول (3 من 3).
  • مصفوفة aud تحتوي على إدخالنا: تم القبول، لأن الـ verifier الخاص بي اختار الإدخال الصحيح قبل أن يراه الـ guard.
  • شرطتان مائلتان في النهاية: تم الرفض. يتم تجاهل شرطة مائلة واحدة فقط.
  • الأصل فقط (https://mcp.example.com): تم الرفض، لأنه يختلف عن https://mcp.example.com/mcp.
  • http:// بدلاً من https://: تم الرفض.

حالة "الأصل فقط" مهمة. تذكر المواصفات https://mcp.example.com كـ URI معياري صالح لخادم.8 الخيار يأخذ قيمة واحدة، لذا يجب أن تساوي ما يصدره خادم التفويض الخاص بك. عند تعيين expectedResource إلى new URL('https://mcp.example.com') (node matrix.mjs url origin)، نجح فقط tok-origin وحصل tok-ours على 401.2

لماذا يفشل الـ audience المكتوب بحروف كبيرة مع مورد نصي (string resource)

الشكل المعياري في المواصفات هو الحروف الصغيرة، ولكنها تنص على أن التنفيذات "يجب أن تقبل مكونات المخطط (scheme) والمضيف (host) بحروف كبيرة من أجل المتانة والتوافق التشغيلي".8

يقوم الـ guard بمقارنة السلاسل النصية المتسلسلة. السلسلة النصية HTTPS://MCP.EXAMPLE.COM/mcp لا تساوي القيمة المتوقعة المكتوبة بحروف صغيرة، لذا تم رفضها في JavaScript البسيط.2

كائن URL يتم تسلسله بحروف صغيرة موحدة، لذا نجح نفس الـ audience. قم بتشغيل node matrix.mjs url لترى tok-upper يتغير من 401 إلى 200.2

الكود المكتوب بأنواع (Typed code) لا يصل عادةً إلى حالة السلسلة النصية. AuthInfo.resource هو URL، وقد رفض مترجم TypeScript وجود resource كسلسلة نصية في فحوصاتي.2 استخدم new URL(...) عند تعبئته، كما يفعل المثال في التوثيق.4

المُتحقق الذي ينسى المورد يمنع الجميع من الدخول

إذا قمت بتعيين expectedResource ولم يقم المُتحقق الخاص بك أبدًا بتعيين AuthInfo.resource، فلن يكون لدى الحارس شيء ليقارنه. في تجربتي، قام node matrix.mjs none برفض جميع الرموز العشرة بـ 401، بما في ذلك tok-ours.2

هذا يؤدي إلى الفشل في حالة الإغلاق (fail closed)، وهو الاتجاه الآمن. تذكر الوثائق أيضًا الحالة الأخرى: "عندما لا يتم تعيين expectedResource، لا يتم مقارنة AuthInfo.resource بأي شيء."4

لا يقوم الـ SDK بقراءة مطالبة aud نيابة عنك. المُتحقق الخاص بك هو من يفعل ذلك ويبلغ عن النتيجة.42

محول Express 2.0.1 يتجاهل expectedResource بصمت

في الإصدار v2، تعيش البرمجية الوسيطة (middleware) لـ Express في حزمة خاصة بها، وهي @modelcontextprotocol/express. تقول ملاحظات إصدار 2.3.0 أن الإصدار 2.0.1 "لا يمرر الخيار، لذا لا يتم مقارنة أي شيء." الحل هو المحول 2.0.2، الذي نُشر في نفس يوم إصدار الخادم 2.3.0.41

لقد اختبرت ذلك باستخدام نفس الخادم الوهمي ونفس خيارات الحارس:2

// v2check.mjs: does the Express adapter actually enforce expectedResource?
import express from 'express';
import { readFileSync } from 'node:fs';
import { requireBearerAuth } from '@modelcontextprotocol/express';
import { SERVER_URL, makeVerifier } from './tokens.mjs';

const version = JSON.parse(readFileSync(new URL('./node_modules/@modelcontextprotocol/express/package.json', import.meta.url))).version;
const app = express();
const guard = requireBearerAuth({ verifier: makeVerifier({ mode: 'url' }), requiredScopes: ['mcp'], expectedResource: SERVER_URL });
app.post('/mcp', guard, (req, res) => res.json({ ok: true }));
const server = await new Promise(r => { const s = app.listen(0, '127.0.0.1', () => r(s)); });

for (const token of ['tok-ours', 'tok-other', 'tok-no-aud']) {
  const r = await fetch(`http://127.0.0.1:${server.address().port}/mcp`, { method: 'POST', headers: { authorization: `Bearer ${token}` } });
  console.log(`@modelcontextprotocol/express ${version}`, token.padEnd(10), r.status);
}
server.close();

مع تثبيت @modelcontextprotocol/server 2.3.1 بجانب كل إصدار من المحول، حصلت على هذه النتيجة:2

@modelcontextprotocol/express 2.0.1 tok-ours   200
@modelcontextprotocol/express 2.0.1 tok-other  200
@modelcontextprotocol/express 2.0.1 tok-no-aud 200
@modelcontextprotocol/express 2.0.2 tok-ours   200
@modelcontextprotocol/express 2.0.2 tok-other  401
@modelcontextprotocol/express 2.0.2 tok-no-aud 401

قم بتثبيتهما معًا: npm i @modelcontextprotocol/server@2.3.1 @modelcontextprotocol/express@2.0.2.

قمت أيضًا بفحص الأنواع (type-check) للمكالمات القصيرة باستخدام TypeScript 5.9.3. في الإصدار 2.0.1، أبلغ المترجم أن expectedResource غير موجود في النوع BearerAuthOptions (TS2353). أما في الإصدار 2.0.2، فقد قبل URL ورفض السلسلة النصية (TS2322).2 أما JavaScript العادي فلا يعطي أي تحذير، لذا فإن تثبيت الإصدار 2.0.1 يتخطى الفحص بصمت.

حدث نفس التجاوز الصامت في الإصدار v1. قم بتثبيت @modelcontextprotocol/sdk@1.31.0 وتشغيل نفس ملف matrix.mjs: يعيد عمود expectedResource القيمة 200 لجميع الرموز العشرة. إذا كان ملف lockfile يثبت الإصدار 1.31.0، فإن الخيار لا يفعل شيئًا في JavaScript العادي حتى عندما يقوم الكود الخاص بك بتعيينه، لذا تحقق من الإصدار المثبت للتحقق من جمهور رمز MCP.2

من البداية للنهاية: عميل MCP حقيقي برمز لخادم آخر

تأتي أكواد الحالة من مسار عادي. للتأكد من أن السلوك يظل ثابتًا من خلال عميل SDK نفسه ومكالمة أداة حقيقية، قمت بتركيب McpServer بأداة واحدة خلف الحارس على SDK 1.32.1.2

// e2e.mjs
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { makeVerifier, SERVER_URL } from './tokens.mjs';

async function start(expectedResource) {
  const app = express();
  app.use(express.json());
  const guard = requireBearerAuth({ verifier: makeVerifier({ mode: 'url' }), requiredScopes: ['mcp'], expectedResource,
    resourceMetadataUrl: 'https://mcp.example.com/.well-known/oauth-protected-resource/mcp' });
  app.post('/mcp', guard, async (req, res) => {
    const server = new McpServer({ name: 'demo', version: '1.0.0' });
    server.registerTool('read_notes', { description: 'Read private notes', inputSchema: {} },
      async () => ({ content: [{ type: 'text', text: `notes for ${req.auth.clientId}` }] }));
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
    res.on('close', () => { transport.close(); server.close(); });
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  });
  const s = await new Promise(r => { const x = app.listen(0, '127.0.0.1', () => r(x)); });
  return { s, url: new URL(`http://127.0.0.1:${s.address().port}/mcp`) };
}

async function call(url, token) {
  const client = new Client({ name: 'c', version: '1.0.0' });
  const t = new StreamableHTTPClientTransport(url, { requestInit: { headers: { Authorization: `Bearer ${token}` } } });
  try {
    await client.connect(t);
    const r = await client.callTool({ name: 'read_notes', arguments: {} });
    return 'OK: ' + r.content[0].text;
  } catch (e) { return 'ERR: ' + String(e.message).slice(0, 140).replace(/\n/g, ' '); }
  finally { await client.close().catch(() => {}); }
}

for (const [label, er] of [['without expectedResource', undefined], ['with expectedResource', SERVER_URL]]) {
  const { s, url } = await start(er);
  console.log(`--- ${label}`);
  for (const tok of ['tok-ours', 'tok-other']) console.log(tok.padEnd(10), await call(url, tok));
  if (er) {
    const r = await fetch(url, { method: 'POST', headers: { authorization: 'Bearer tok-other', 'content-type': 'application/json' }, body: '{}' });
    console.log('HTTP', r.status, '| WWW-Authenticate:', r.headers.get('www-authenticate'));
  }
  s.close();
}

النتيجة:2

--- without expectedResource
tok-ours   OK: notes for client-1
tok-other  OK: notes for client-1
--- with expectedResource
tok-ours   OK: notes for client-1
tok-other  ERR: Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token","error_description":"Token was not issued for this resource"}
HTTP 401 | WWW-Authenticate: Bearer error="invalid_token", error_description="Token was not issued for this resource", scope="mcp", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"

بدون هذا الخيار، كان العميل الذي يحمل توكن لخادم آخر يستدعي read_notes ويحصل على نتيجة طبيعية. أما مع تفعيل الخيار، فقد رد الحارس بـ 401 وحصل العميل على خطأ بدلاً من نتيجة الأداة.

تحمل استجابة 401 تحدي WWW-Authenticate مع resource_metadata، وهو الرابط الذي يتبعه العملاء في تدفق التفويض الخاص بالمواصفات للعثور على خادم التفويض.3

قائمة مراجعة إعداد OAuth لخادم MCP من أجل جمهور التوكن (token audience)

قم بتنفيذ هذه الخطوات بالترتيب:

  1. قم بالتحديث إلى SDK 1.32.1 أو @modelcontextprotocol/server 2.3.1 (ظهر الخيار في الإصدارين 1.32.0 و 2.3.0). في Express، قم بتحديث @modelcontextprotocol/express إلى 2.0.2 أو إصدار أحدث.41
  2. حدد الرابط (URL) الذي يضعه خادم التفويض الخاص بك في aud لخادمك. اضبط expectedResource على هذه القيمة بالضبط، على سبيل المثال new URL('https://mcp.example.com/mcp').
  3. في أداة التحقق (verifier) الخاصة بك، اقرأ مطالبة aud (أو aud في استجابة الاستبطاق)، واختر إدخال خادمك، وأرجعه كـ new URL(...) في AuthInfo.resource. إذا كان المعرف الخاص بك ليس شيئاً يمكن لـ new URL() تحليله، مثل GUID مجرد، فإن طلب السحب (PR) ينصح بالإبقاء على المقارنة في أداة التحقق الخاصة بك وترك الخيار غير مضبوط.5
  4. أرجع دائماً expiresAt. تذكر الوثائق أن التوكن الذي لا يحتوي على expiresAt يحصل على 401.4
  5. اختبر باستخدام توكن تم إصداره لمورد مختلف. إذا حصلت على 200، فإن الفحص لا يؤدي وظيفته.

في بيئة الإنتاج، ستقوم أداة التحقق الخاصة بك بفحص توقيع JWT أو استدعاء استبطاق التوكن أولاً. خادمي الوهمي يتخطى ذلك ليبقى سلوك الجمهور (audience) مرئياً.

ما لم أختبره

تستخدم هذه الاختبارات خادم تفويض وهمي ومسار محاكى. لم أختبر مزود هوية حقيقي، أو فحص توقيع JWT، أو استبطاق التوكن.

قمت بتشغيل Node 22.22.0 على Linux x86_64 فقط. اختبارات وقت التشغيل هي JavaScript بسيطة، وفحوصات الأنواع كانت مجرد بضعة أسطر من TypeScript، وليست مشروعاً حقيقياً.

لم أنشر اختبارات لمحولات Hono أو Fastify، أو لبوابة fetch المعيارية للويب requireBearerAuth.

لم أختبر maxToolInputElements، والتي صدرت في نفس الإصدارات.1

الخلاصة

تنص مواصفات MCP على أن الخوادم يجب أن تتحقق من الـ audience الخاص بالتوكن. توفر SDK TypeScript الآن خياراً لذلك، ولكنه يعمل فقط إذا قمت بتعيينه، وكان الـ verifier الخاص بك يبلغ عن resource، وكان محول Express الخاص بك بإصدار 2.0.2 أو أحدث.

للإعدادات ذات الصلة، راجع درس خادم MCP للإنتاج مع OAuth و Streamable HTTP وتحليل بروتوكول MCP عديم الحالة وتفويض المؤسسات. وبالنسبة للمخاطر من جانب الأدوات، راجع كشف انحراف أدوات AI SDK و MCP rug pulls.

Footnotes

  1. modelcontextprotocol/typescript-sdk GitHub releases, read on 2026-10-06: 1.32.0 (published 2026-10-02), @modelcontextprotocol/server@2.3.0 (2026-10-02), 1.32.1 and v2.3.1 (2026-10-05). The quoted 1.32.0 lines are from that release's notes; the 2.3.0 and 2.3.1 wording matches the CHANGELOG.md entries in the repository's packages/server, packages/middleware/express and packages/server-legacy folders. Package publish times, from npm view <package> time: @modelcontextprotocol/sdk 1.32.0 on 2026-10-02, @modelcontextprotocol/express 2.0.2 on 2026-10-02. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9

  2. Author's measurement, October 6, 2026: Node 22.22.0 on Linux x86_64, Express 5.2.1, TypeScript 5.9.3, @modelcontextprotocol/sdk 1.31.0 and 1.32.1, @modelcontextprotocol/server 2.3.1 with @modelcontextprotocol/express 2.0.1 and 2.0.2. A fake authorization server with ten tokens; no real identity provider. The code in this post was extracted from the finished post and re-run in fresh folders, and the output matches. matrix.mjs was run three times per mode, string and url, with identical output within each mode. Figure 1 was drawn from runs of the same ten tokens; its two v2 columns came from a harness equivalent to v2check.mjs that sends all ten tokens. I read the installed bearerAuth.js of 1.32.1 for the order of checks, and searched the installed v1 server folder and the v2 server, express and core packages for reads of an aud claim; the only hit was a code comment in the v2 type definitions. The type checks were small requireBearerAuth calls compiled with --strict against each installed package, including a verifier that returns a string resource on 1.32.1. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23

  3. Model Context Protocol, "Authorization" (Token Handling section) and "Authorization Security Considerations" (Token Audience Binding and Validation; Access Token Privilege Restriction), specification version 2026-07-28, fetched 2026-10-06. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  4. modelcontextprotocol/typescript-sdk, docs/serving/authorization.md on the main branch, read 2026-10-06, including its guidance on expectedResource, AuthInfo.resource and @modelcontextprotocol/express 2.0.1. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  5. Pull request #2929, "feat(server): add expectedResource to the bearer-token check", opened and merged 2026-10-02, fetched 2026-10-06. ↩ ↩2 ↩3 ↩4

  6. Model Context Protocol, "Versioning", "Revisions" section, which states that the current protocol version is 2026-07-28. Fetched 2026-10-06. ↩

  7. IETF, RFC 8707, "Resource Indicators for OAuth 2.0", Proposed Standard, February 2020; authors Brian Campbell, John Bradley and Hannes Tschofenig. ↩

  8. Model Context Protocol, "Authorization", "Resource Parameter Implementation" and "Canonical Server URI" sections, specification version 2026-07-28, fetched 2026-10-06. ↩ ↩2

الأسئلة الشائعة

نعم. تذكر صفحة التفويض بتاريخ 2026-07-28 أنه يجب على الخوادم التحقق من أن توكنات الوصول قد صدرت خصيصاً لها كجمهور مستهدف، وفقاً لـ RFC 8707 القسم 2.3