فحص جمهور MCP Token: تم اختبار expectedResource (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
الشكل 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)
قم بتنفيذ هذه الخطوات بالترتيب:
- قم بالتحديث إلى SDK 1.32.1 أو
@modelcontextprotocol/server2.3.1 (ظهر الخيار في الإصدارين 1.32.0 و 2.3.0). في Express، قم بتحديث@modelcontextprotocol/expressإلى 2.0.2 أو إصدار أحدث.41 - حدد الرابط (URL) الذي يضعه خادم التفويض الخاص بك في
audلخادمك. اضبطexpectedResourceعلى هذه القيمة بالضبط، على سبيل المثالnew URL('https://mcp.example.com/mcp'). - في أداة التحقق (verifier) الخاصة بك، اقرأ مطالبة
aud(أوaudفي استجابة الاستبطاق)، واختر إدخال خادمك، وأرجعه كـnew URL(...)فيAuthInfo.resource. إذا كان المعرف الخاص بك ليس شيئاً يمكن لـnew URL()تحليله، مثل GUID مجرد، فإن طلب السحب (PR) ينصح بالإبقاء على المقارنة في أداة التحقق الخاصة بك وترك الخيار غير مضبوط.5 - أرجع دائماً
expiresAt. تذكر الوثائق أن التوكن الذي لا يحتوي علىexpiresAtيحصل على 401.4 - اختبر باستخدام توكن تم إصداره لمورد مختلف. إذا حصلت على 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
-
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.1andv2.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 theCHANGELOG.mdentries in the repository'spackages/server,packages/middleware/expressandpackages/server-legacyfolders. Package publish times, fromnpm view <package> time:@modelcontextprotocol/sdk1.32.0 on 2026-10-02,@modelcontextprotocol/express2.0.2 on 2026-10-02. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Author's measurement, October 6, 2026: Node 22.22.0 on Linux x86_64, Express 5.2.1, TypeScript 5.9.3,
@modelcontextprotocol/sdk1.31.0 and 1.32.1,@modelcontextprotocol/server2.3.1 with@modelcontextprotocol/express2.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.mjswas run three times per mode,stringandurl, 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 tov2check.mjsthat sends all ten tokens. I read the installedbearerAuth.jsof 1.32.1 for the order of checks, and searched the installed v1serverfolder and the v2server,expressandcorepackages for reads of anaudclaim; the only hit was a code comment in the v2 type definitions. The type checks were smallrequireBearerAuthcalls compiled with--strictagainst each installed package, including a verifier that returns a stringresourceon 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 -
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
-
modelcontextprotocol/typescript-sdk,
docs/serving/authorization.mdon the main branch, read 2026-10-06, including its guidance onexpectedResource,AuthInfo.resourceand@modelcontextprotocol/express2.0.1. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
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
-
Model Context Protocol, "Versioning", "Revisions" section, which states that the current protocol version is 2026-07-28. Fetched 2026-10-06. ↩
-
IETF, RFC 8707, "Resource Indicators for OAuth 2.0", Proposed Standard, February 2020; authors Brian Campbell, John Bradley and Hannes Tschofenig. ↩
-
Model Context Protocol, "Authorization", "Resource Parameter Implementation" and "Canonical Server URI" sections, specification version 2026-07-28, fetched 2026-10-06. ↩ ↩2


