mcp-tool-auth.e2e.spec.ts•5.98 kB
import { INestApplication, Injectable } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { z } from 'zod';
import { Context, Tool } from '../src';
import { McpModule } from '../src/mcp/mcp.module';
import { Progress } from '@modelcontextprotocol/sdk/types.js';
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { createSseClient } from './utils';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
// Mock authentication guard
class MockAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    if (
      request.headers.authorization &&
      request.headers.authorization.includes('token-xyz')
    ) {
      request.user = {
        id: 'user123',
        name: 'Test User',
        orgMemberships: [
          {
            orgId: 'org123',
            organization: {
              name: 'Auth Test Org',
            },
          },
        ],
      };
      return true;
    }
    return false;
  }
}
// Mock user repository
@Injectable()
class MockUserRepository {
  async findOne() {
    return Promise.resolve({
      id: 'userRepo123',
      name: 'Repository User',
      orgMemberships: [
        {
          orgId: 'org123',
          organization: {
            name: 'Repository Org',
          },
        },
      ],
    });
  }
}
// Greeting tool that uses the authentication context
@Injectable()
export class AuthGreetingTool {
  constructor(private readonly userRepository: MockUserRepository) {}
  @Tool({
    name: 'auth-hello-world',
    description: 'A sample tool that accesses the authenticated user',
    parameters: z.object({
      name: z.string().default('World'),
    }),
  })
  async sayHello({ name }, context: Context, request: Request & { user: any }) {
    // Access both repository data and the authenticated user context
    const repoUser = await this.userRepository.findOne();
    const authUser = request.user; // Authenticated user from the request
    // Construct greeting using both data sources
    const greeting = `Hello, ${name}! I'm ${authUser.name} from ${authUser.orgMemberships[0].organization.name}. Repository user is ${repoUser.name}.`;
    // Report progress for demonstration
    for (let i = 0; i < 5; i++) {
      await new Promise((resolve) => setTimeout(resolve, 200));
      await context.reportProgress({
        progress: (i + 1) * 20,
        total: 100,
      } as Progress);
    }
    return {
      content: [
        {
          type: 'text',
          text: greeting,
        },
      ],
    };
  }
}
describe('E2E: MCP Server Tool with Authentication', () => {
  let app: INestApplication;
  let testPort: number;
  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
        McpModule.forRoot({
          name: 'test-auth-mcp-server',
          version: '0.0.1',
          // Specify the MockAuthGuard to protect the messages endpoint
          guards: [MockAuthGuard],
          capabilities: {
            resources: {},
            resourceTemplates: {},
            prompts: {},
            tools: {
              'auth-hello-world': {
                description:
                  'A sample tool that accesses the authenticated user',
                input: {
                  name: {
                    type: 'string',
                    default: 'World',
                  },
                },
              },
            },
          },
        }),
      ],
      providers: [AuthGreetingTool, MockUserRepository, MockAuthGuard],
    }).compile();
    app = moduleFixture.createNestApplication();
    await app.listen(0);
    const server = app.getHttpServer();
    testPort = server.address().port;
  });
  afterAll(async () => {
    await app.close();
  });
  it('should list tools', async () => {
    const client = await createSseClient(testPort, {
      requestInit: {
        headers: {
          Authorization: 'Bearer token-xyz',
        },
      },
    });
    const tools = await client.listTools();
    // Verify that the authenticated tool is available
    expect(tools.tools.length).toBeGreaterThan(0);
    expect(
      tools.tools.find((t) => t.name === 'auth-hello-world'),
    ).toBeDefined();
    await client.close();
  });
  it('should inject authentication context into the tool', async () => {
    const client = await createSseClient(testPort, {
      requestInit: {
        headers: {
          Authorization: 'Bearer token-xyz',
        },
      },
    });
    let progressCount = 0;
    const result: any = await client.callTool(
      {
        name: 'auth-hello-world',
        arguments: { name: 'Authenticated User' },
      },
      undefined,
      {
        onprogress: () => {
          progressCount++;
        },
      },
    );
    // Verify that progress notifications were received
    expect(progressCount).toBeGreaterThan(0);
    // Verify that authentication context was available to the tool
    expect(result.content[0].type).toBe('text');
    expect(result.content[0].text).toContain('Auth Test Org');
    expect(result.content[0].text).toContain('Test User');
    expect(result.content[0].text).toContain(
      'Repository user is Repository User',
    );
    await client.close();
  });
  it('should reject unauthenticated connections', async () => {
    // Connection should be rejected
    let client: Client | undefined;
    try {
      client = await createSseClient(testPort, {
        requestInit: {
          headers: {
            Authorization: 'Bearer invalid-token',
          },
        },
      });
      // If we get here, the test should fail
      fail('Connection should have been rejected');
    } catch (error) {
      // We expect an error to be thrown when authentication fails
      expect(error).toBeDefined();
    } finally {
      await client?.close();
    }
  });
});