@@ -21,10 +21,17 @@ import {
2121} from '@modelcontextprotocol/server' ;
2222import type { OAuthTokenVerifier , AuthInfo } from '@modelcontextprotocol/server' ;
2323import { z } from 'zod' ;
24- import express , { Request , Response } from 'express' ;
24+ import express , { Request , Response , NextFunction } from 'express' ;
2525import cors from 'cors' ;
2626import { randomUUID } from 'crypto' ;
2727
28+ // Extend Express Request type to include auth info from SDK middleware
29+ declare module 'express' {
30+ interface Request {
31+ auth ? : AuthInfo ;
32+ }
33+ }
34+
2835// Check for required environment variable
2936const AUTH_SERVER_URL = process . env . MCP_CONFORMANCE_AUTH_SERVER_URL ;
3037if ( ! AUTH_SERVER_URL ) {
@@ -86,6 +93,23 @@ function createMcpServer(): McpServer {
8693 }
8794 ) ;
8895
96+ // Privileged tool requiring 'admin' scope - for step-up auth testing
97+ mcpServer . tool (
98+ 'admin-action' ,
99+ 'A privileged action that requires admin scope - used for step-up auth testing' ,
100+ {
101+ action : z . string ( ) . optional ( ) . describe ( 'The admin action to perform' )
102+ } ,
103+ async ( args : { action ?: string } ) => {
104+ const action = args . action || 'default-admin-action' ;
105+ return {
106+ content : [
107+ { type : 'text' , text : `Admin action performed: ${ action } ` }
108+ ]
109+ } ;
110+ }
111+ ) ;
112+
89113 return mcpServer ;
90114}
91115
@@ -154,6 +178,67 @@ function isInitializeRequest(body: unknown): boolean {
154178 ) ;
155179}
156180
181+ // Helper to check if request is a tools/call for admin-action
182+ function isAdminToolCall ( body : unknown ) : boolean {
183+ if (
184+ typeof body !== 'object' ||
185+ body === null ||
186+ ! ( 'method' in body ) ||
187+ ! ( 'params' in body )
188+ ) {
189+ return false ;
190+ }
191+ const { method , params } = body as { method : string ; params : unknown } ;
192+ if ( method !== 'tools/call' ) {
193+ return false ;
194+ }
195+ if (
196+ typeof params !== 'object' ||
197+ params === null ||
198+ ! ( 'name' in params )
199+ ) {
200+ return false ;
201+ }
202+ return ( params as { name : string } ) . name === 'admin-action' ;
203+ }
204+
205+ // Scope required for admin-action tool
206+ const ADMIN_SCOPE = 'admin' ;
207+
208+ /**
209+ * Middleware to check for admin scope on privileged tool calls.
210+ * Returns 403 insufficient_scope if the token doesn't have admin scope.
211+ */
212+ function checkAdminScope ( prmUrl : string ) {
213+ return ( req : Request , res : Response , next : NextFunction ) : void => {
214+ // Only check for tools/call with admin-action
215+ if ( ! isAdminToolCall ( req . body ) ) {
216+ return next ( ) ;
217+ }
218+
219+ // req.auth is set by requireBearerAuth middleware
220+ const scopes = req . auth ?. scopes || [ ] ;
221+
222+ if ( ! scopes . includes ( ADMIN_SCOPE ) ) {
223+ // Return 403 with insufficient_scope error
224+ res . setHeader (
225+ 'WWW-Authenticate' ,
226+ `Bearer error="insufficient_scope", ` +
227+ `scope="${ ADMIN_SCOPE } ", ` +
228+ `resource_metadata="${ prmUrl } ", ` +
229+ `error_description="The admin-action tool requires admin scope"`
230+ ) ;
231+ res . status ( 403 ) . json ( {
232+ error : 'insufficient_scope' ,
233+ error_description : 'The admin-action tool requires admin scope'
234+ } ) ;
235+ return ;
236+ }
237+
238+ next ( ) ;
239+ } ;
240+ }
241+
157242// ===== EXPRESS APP =====
158243
159244async function startServer ( ) {
@@ -186,6 +271,9 @@ async function startServer() {
186271 resourceMetadataUrl: prmUrl
187272 });
188273
274+ // Create scope-checking middleware for privileged tools
275+ const adminScopeCheck = checkAdminScope(prmUrl);
276+
189277 const app = express();
190278 app.use(express.json());
191279
@@ -209,13 +297,15 @@ async function startServer() {
209297 (_req: Request, res: Response) => {
210298 res.json({
211299 resource: getBaseUrl(),
212- authorization_servers : [ AUTH_SERVER_URL ]
300+ authorization_servers: [AUTH_SERVER_URL],
301+ // List supported scopes for step-up auth testing
302+ scopes_supported: [ADMIN_SCOPE]
213303 });
214304 }
215305 );
216306
217- // Handle POST requests to /mcp with bearer auth
218- app . post ( '/mcp' , bearerAuth , async ( req : Request , res : Response ) => {
307+ // Handle POST requests to /mcp with bearer auth and scope checking
308+ app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
219309 const sessionId = req.headers['mcp-session-id'] as string | undefined;
220310
221311 try {
0 commit comments