Skip to Content
DocsTutorialImplementing Basic Governance

Tutorial: Implementing Basic Governance (Identity & RBAC) with @ithena-one/mcp-governance

This tutorial guides you through incrementally adding Identity Resolution and Role-Based Access Control (RBAC) to a Model Context Protocol (MCP) server using the @ithena-one/mcp-governance SDK. We’ll start with a basic setup and progressively enhance a single application file (src/governed-app.ts).

Prerequisites:

  • Node.js (v18 or later recommended)
  • npm, yarn, or pnpm
  • Basic understanding of TypeScript
  • Familiarity with the base @modelcontextprotocol/sdk
  • @ithena-one/mcp-governance SDK installed (e.g., npm install @ithena-one/mcp-governance)

To create a governed MCP server that identifies callers and enforces basic permissions using RBAC, demonstrating the core workflow of the SDK.


Step 0: Project Setup & Initial Governed Server

We need a starting point: a working MCP server using the governance SDK, but with most governance features off or using defaults. This ensures the basic SDK setup is correct before adding complexity.

Create Project & Install Dependencies

Terminal
mkdir my-governed-mcp-app cd my-governed-mcp-app npm init -y npm install @modelcontextprotocol/sdk @ithena-one/mcp-governance zod npm install --save-dev typescript @types/node npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib esnext --module nodenext --moduleResolution nodenext --strict mkdir src

Create src/governed-app.ts

This initial version uses GovernedServer but only configures the default console logger and auditor. No identity or RBAC yet.

src/governed-app.ts
// src/governed-app.ts import { Server as BaseServer } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import process from 'node:process'; // --- Import Governance SDK --- import { GovernedServer, GovernedServerOptions, ConsoleLogger, // Default logger ConsoleAuditLogStore, // Default auditor GovernedRequestHandlerExtra, // Type for handler context // We'll import interfaces as needed in later steps } from '@ithena-one/mcp-governance'; console.log('Starting Governed MCP Server...'); // --- 1. Create Base MCP Server --- const baseServer = new BaseServer( { name: "MyGovernedMCPServer", version: "1.0.0" }, { capabilities: { tools: {} } } // Enable tools capability ); // --- 2. Governance Components (Initial Defaults) --- const logger = new ConsoleLogger({}, 'debug'); // Log debug and above const auditStore = new ConsoleAuditLogStore(); // Log audits to console (NOT FOR PRODUCTION) // --- 3. GovernedServer Configuration (Initial) --- const governedServerOptions: GovernedServerOptions = { logger: logger, auditStore: auditStore, serviceIdentifier: "governed-app-instance", enableRbac: false, // Explicitly OFF for Step 0 }; // --- 4. Create GovernedServer instance --- const governedServer = new GovernedServer(baseServer, governedServerOptions); logger.info('GovernedServer created'); // --- 5. Define Tool Schema --- const helloToolSchema = z.object({ jsonrpc: z.literal("2.0"), id: z.union([z.string(), z.number()]), method: z.literal('tools/callHello'), // Unique method name params: z.object({ arguments: z.object({ greeting: z.string().optional().default('Hello') }).optional().default({ greeting: 'Hello' }), testUserId: z.string().optional(), // We'll use this later for stdio identity testing _meta: z.any().optional() }) }); // Add schema for sensitive tool here to avoid errors later const sensitiveToolSchema = z.object({ jsonrpc: z.literal("2.0"), id: z.union([z.string(), z.number()]), method: z.literal('tools/callSensitive'), params: z.object({ arguments: z.any().optional(), testUserId: z.string().optional(), _meta: z.any().optional() }) }); // --- 6. Register Handler --- governedServer.setRequestHandler(helloToolSchema, async (request, extra: GovernedRequestHandlerExtra) => { const scopedLogger = extra.logger || logger; // Identity will be null here scopedLogger.info(`[Handler] Executing callHello. EventID: ${extra.eventId}`); const greeting = request.params?.arguments?.greeting || 'DefaultGreeting'; const responseText = `${greeting} World from governed server!`; // No identity yet return { content: [{ type: 'text', text: responseText }] }; } ); logger.info('Handler registered.'); // --- 7. Connect and Shutdown --- const transport = new StdioServerTransport(); async function startServer() { try { await governedServer.connect(transport); logger.info("Governed MCP server (Step 0) started on stdio."); logger.info("Ready for requests..."); } catch (error) { logger.error("Failed to start server", error); process.exit(1); } } const shutdown = async () => { logger.info("Shutting down..."); try { await governedServer.close(); logger.info("Shutdown complete."); process.exit(0); } catch (err) { logger.error("Error during shutdown:", err); process.exit(1); } }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); startServer(); // Call startServer at the end

Build and Run

Terminal
npm run build npm run start

Test

Send a request via stdin:

stdin_request.json
{"jsonrpc":"2.0","id":1,"method":"tools/callHello","params":{"arguments":{"greeting":"Initial"}}}

Request succeeds. Logs show handler execution. Audit record shows identity: null and authorization: {decision: 'not_applicable'}. Stop server (Ctrl+C).

Step 1: Implement Identity Resolution

The first step in governance is knowing who is making the request. We need to implement the IdentityResolver interface and configure the GovernedServer to use it.

Modify src/governed-app.ts:

Add Imports

Add necessary types/interfaces near the top:

src/governed-app.ts
// Add near other governance imports import { IdentityResolver, OperationContext, UserIdentity } from '@ithena-one/mcp-governance';

Implement Resolver

Define the logic before governedServerOptions. We use a parameter for stdio testing.

src/governed-app.ts
// --- ADDED FOR STEP 1: IdentityResolver --- const testIdentityResolver: IdentityResolver = { async resolveIdentity(opCtx: OperationContext): Promise<UserIdentity | null> { const scopedLogger = opCtx.logger || logger; // Use context logger scopedLogger.debug('Entering IdentityResolver', { eventId: opCtx.eventId }); // Check param first for stdio testing const paramsObj = opCtx.mcpMessage.params as any; const testIdParam = paramsObj?.testUserId; if (testIdParam) { scopedLogger.info(`Identity resolved via param: ${testIdParam}`, { eventId: opCtx.eventId }); return { id: testIdParam, source: 'param' }; // Return structured identity } // Fallback to header (for potential future SSE/HTTP testing) const userHeader = opCtx.transportContext.headers?.['x-test-user-id']; const userIdFromHeader = Array.isArray(userHeader) ? userHeader[0] : userHeader; if (userIdFromHeader) { scopedLogger.info(`Identity resolved via header: ${userIdFromHeader}`, { eventId: opCtx.eventId }); return { id: userIdFromHeader, source: 'header' }; } scopedLogger.info('No test identity found', { eventId: opCtx.eventId }); return null; } }; // --- END STEP 1 ---

Update governedServerOptions

Add the identityResolver.

src/governed-app.ts
// --- 3. GovernedServer Configuration (Initial) --- const governedServerOptions: GovernedServerOptions = { logger: logger, auditStore: auditStore, identityResolver: testIdentityResolver, // <-- ADDED serviceIdentifier: "governed-app-instance", enableRbac: false, // Still OFF for this step };

Update Handler

Modify the helloToolSchema handler to use the resolved identity from extra.identity.

src/governed-app.ts
// Find the setRequestHandler call for helloToolSchema governedServer.setRequestHandler(helloToolSchema, async (request, extra: GovernedRequestHandlerExtra) => { const scopedLogger = extra.logger || logger; const identityId = typeof extra.identity === 'string' ? extra.identity : extra.identity?.id; // Get ID scopedLogger.info(`[Handler] Executing callHello for identity: ${identityId || 'anonymous'}. EventID: ${extra.eventId}`); const greeting = request.params?.arguments?.greeting || 'DefaultGreeting'; const responseText = `${greeting} ${identityId || 'World'} from governed server!`; // Use identity in response return { content: [{ type: 'text', text: responseText }] }; } );

Rebuild and Run

Terminal
npm run build npm run start

Test

  • Without Identity: Send {"jsonrpc":"2.0","id":2,"method":"tools/callHello"}

    Succeeds, response includes “World”. Logs: “No test identity found”. Audit: identity: null.

  • With Identity: Send {"jsonrpc":"2.0","id":3,"method":"tools/callHello","params":{"testUserId": "tester"}}

    Succeeds, response includes “tester”. Logs: “Identity resolved via param”. Audit: shows identity object. Stop server.

Step 2: Implement Basic RBAC

Now that we know who the user is, we need to check what they are allowed to do. We implement RoleStore and PermissionStore and enable the RBAC check in the pipeline.

Modify src/governed-app.ts:

Add Imports

src/governed-app.ts
// Add near other governance imports import { RoleStore, PermissionStore, InMemoryRoleStore, InMemoryPermissionStore, } from '@ithena-one/mcp-governance';

Implement RBAC Stores

Define roles/permissions before governedServerOptions.

src/governed-app.ts
// --- ADDED FOR STEP 2: RBAC Stores --- const testRoleStore: RoleStore = new InMemoryRoleStore({ 'admin-007': ['admin'], // User 'admin-007' has 'admin' role 'user-123': ['user'], // User 'user-123' has 'user' role }); const testPermissionStore: PermissionStore = new InMemoryPermissionStore({ // Define permissions granted by each role 'admin': ['tool:callHello', 'tool:callSensitive'], // Use derived permission strings 'user': ['tool:callHello'], }); // --- END STEP 2 ---

Update governedServerOptions

Add stores and set enableRbac: true.

src/governed-app.ts
// --- 3. GovernedServer Configuration --- const governedServerOptions: GovernedServerOptions = { logger: logger, auditStore: auditStore, identityResolver: testIdentityResolver, roleStore: testRoleStore, // <-- ADDED permissionStore: testPermissionStore, // <-- ADDED enableRbac: true, // <-- ENABLED auditDeniedRequests: true, // <-- Good practice serviceIdentifier: "governed-app-instance", };

Add Sensitive Tool Handler

Add the handler for sensitiveToolSchema near the helloToolSchema handler.

src/governed-app.ts
// --- ADDED FOR STEP 2 --- governedServer.setRequestHandler(sensitiveToolSchema, async (request, extra: GovernedRequestHandlerExtra) => { const identityId = typeof extra.identity === 'string' ? extra.identity : extra.identity?.id; const scopedLogger = extra.logger || logger; // Log roles received by the handler scopedLogger.info(`[Handler] Executing callSensitive for identity: ${identityId}`, { roles: extra.roles }); // RBAC check 'tool:callSensitive' must have passed to reach here return { content: [{ type: 'text', text: `Sensitive data accessed by ${identityId}` }] }; } ); // --- END STEP 2 ---

Update Hello Handler Log (Optional)

Add role logging.

src/governed-app.ts
// Modify helloTool handler log scopedLogger.info(`[Handler] Executing callHello for identity: ${identityId || 'anonymous'} with roles: ${JSON.stringify(extra.roles)}. EventID: ${extra.eventId}`);

Rebuild and Run

Terminal
npm run build npm run start

Test RBAC Scenarios

  • Hello (User): Send {"jsonrpc":"2.0","id":4,"method":"tools/callHello","params":{"testUserId": "user-123"}}

    Success. Logs: ID resolved, roles ['user'], permission tool:callHello granted.

  • Sensitive (User): Send {"jsonrpc":"2.0","id":5,"method":"tools/callSensitive","params":{"testUserId": "user-123"}}

    Error -32001. Logs: ID resolved, roles ['user'], permission tool:callSensitive derived, but check fails. Audit: denied, reason: 'permission'.

  • Sensitive (Admin): Send {"jsonrpc":"2.0","id":6,"method":"tools/callSensitive","params":{"testUserId": "admin-007"}}

    Success. Logs: ID resolved, roles ['admin'], permission tool:callSensitive granted. Handler runs.

  • Hello (No ID): Send {"jsonrpc":"2.0","id":7,"method":"tools/callHello"}}

    Error -32001. Logs: ID resolved as null. RBAC fails immediately (reason: 'identity'). Stop server.


Final Code (src/governed-app.ts - Identity & RBAC)

After completing Step 2, your src/governed-app.ts should look similar to this:

src/governed-app.ts
// src/governed-app.ts import { Server as BaseServer } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import process from 'node:process'; // --- Import Governance SDK --- import { GovernedServer, GovernedServerOptions, ConsoleLogger, ConsoleAuditLogStore, InMemoryRoleStore, InMemoryPermissionStore, IdentityResolver, RoleStore, PermissionStore, OperationContext, UserIdentity, GovernedRequestHandlerExtra, } from '@ithena-one/mcp-governance'; console.log('Starting Governed MCP Server (Identity & RBAC)...'); // --- 1. Create Base MCP Server --- const baseServer = new BaseServer( { name: "MyGovernedMCPServer-RBAC", version: "1.0.0" }, { capabilities: { tools: {} } } ); // --- 2. Governance Components Implementations --- const logger = new ConsoleLogger({}, 'debug'); const auditStore = new ConsoleAuditLogStore(); // Not for Production const testIdentityResolver: IdentityResolver = { async resolveIdentity(opCtx: OperationContext): Promise<UserIdentity | null> { const scopedLogger = opCtx.logger || logger; scopedLogger.debug('Entering IdentityResolver', { eventId: opCtx.eventId }); const paramsObj = opCtx.mcpMessage.params as any; const testIdParam = paramsObj?.testUserId; if (testIdParam) { scopedLogger.info(`Identity resolved via param: ${testIdParam}`, { eventId: opCtx.eventId }); return { id: testIdParam, source: 'param' }; } const userHeader = opCtx.transportContext.headers?.['x-test-user-id']; const userIdFromHeader = Array.isArray(userHeader) ? userHeader[0] : userHeader; if (userIdFromHeader) { scopedLogger.info(`Identity resolved via header: ${userIdFromHeader}`, { eventId: opCtx.eventId }); return { id: userIdFromHeader, source: 'header' }; } scopedLogger.info('No test identity found', { eventId: opCtx.eventId }); return null; } }; const testRoleStore: RoleStore = new InMemoryRoleStore({ 'admin-007': ['admin'], 'user-123': ['user'], }); const testPermissionStore: PermissionStore = new InMemoryPermissionStore({ 'admin': ['tool:callHello', 'tool:callSensitive'], 'user': ['tool:callHello'], }); // --- 3. Final GovernedServer Configuration --- const governedServerOptions: GovernedServerOptions = { logger: logger, auditStore: auditStore, identityResolver: testIdentityResolver, roleStore: testRoleStore, permissionStore: testPermissionStore, enableRbac: true, // RBAC is ON auditDeniedRequests: true, serviceIdentifier: "governed-app-rbac-instance", }; // --- 4. Create Final GovernedServer instance --- const governedServer = new GovernedServer(baseServer, governedServerOptions); logger.info('GovernedServer created with Identity & RBAC options'); // --- 5. Define Tool Schemas --- const helloToolSchema = z.object({ jsonrpc: z.literal("2.0"), id: z.union([z.string(), z.number()]), method: z.literal('tools/callHello'), params: z.object({ arguments: z.object({ greeting: z.string().optional().default('Hello') }).optional().default({ greeting: 'Hello' }), testUserId: z.string().optional(), _meta: z.any().optional() }) }); const sensitiveToolSchema = z.object({ jsonrpc: z.literal("2.0"), id: z.union([z.string(), z.number()]), method: z.literal('tools/callSensitive'), params: z.object({ arguments: z.any().optional(), testUserId: z.string().optional(), _meta: z.any().optional() }) }); // --- 6. Register Handlers --- governedServer.setRequestHandler(helloToolSchema, async (request, extra: GovernedRequestHandlerExtra) => { const scopedLogger = extra.logger || logger; const identityId = typeof extra.identity === 'string' ? extra.identity : extra.identity?.id; scopedLogger.info(`[Handler] Executing callHello for identity: ${identityId || 'anonymous'} with roles: ${JSON.stringify(extra.roles)}. EventID: ${extra.eventId}`); const greeting = request.params?.arguments?.greeting || 'DefaultGreeting'; const responseText = `${greeting} ${identityId || 'World'} from governed server!`; return { content: [{ type: 'text', text: responseText }] }; }); governedServer.setRequestHandler(sensitiveToolSchema, async (request, extra: GovernedRequestHandlerExtra) => { const scopedLogger = extra.logger || logger; const identityId = typeof extra.identity === 'string' ? extra.identity : extra.identity?.id; scopedLogger.info(`[Handler] Executing callSensitive for identity: ${identityId}`, { roles: extra.roles }); return { content: [{ type: 'text', text: `Sensitive data accessed by ${identityId}` }] }; }); logger.info('Handlers registered.'); // --- 7. Connect and Shutdown --- const transport = new StdioServerTransport(); async function startServer() { try { await governedServer.connect(transport); logger.info("Governed MCP server (Identity & RBAC) started on stdio."); logger.info("Ready for requests..."); } catch (error) { logger.error("Failed to start server", error as Error); process.exit(1); } } const shutdown = async () => { logger.info("Shutting down..."); try { await governedServer.close(); logger.info("Shutdown complete."); process.exit(0); } catch (err) { logger.error("Error during shutdown:", err); process.exit(1); } }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); startServer();

Next Steps & Production Considerations

This tutorial covered the core of Identity and RBAC.

The @ithena-one/mcp-governance SDK also supports:

  • Credential Resolution: Securely injecting secrets (CredentialResolver).
  • Custom Audit Sanitization: Preventing data leaks (sanitizeForAudit).
  • Hooks: Running custom logic (postAuthorizationHook).
⚠️

This tutorial used simple, non-production-ready components.

  1. Replace default resolvers and stores with implementations integrated with your actual Auth systems.
  2. Replace default logger/auditor with production-ready solutions.
  3. Implement and thoroughly test a custom sanitizeForAudit function.
  4. Consider using CredentialResolver for secrets.
  5. Secure your transport (TLS).

Building robust, secure governance requires careful implementation. If managing these components becomes complex, exploring managed platforms designed for MCP governance might be beneficial.

Alternatively, consider the Ithena managed platform. Instead of building and maintaining your own governance components and logic, integrate our managed solution via a simple configuration pointing to the Ithena endpoint. Gain robust governance with significantly less effort. Visit https://ithena.one to learn more.

Last updated on