@postman/test-mcp-server
v1.0.0
Published
MCP Server for E2E testing of the Postman MCP Client
Downloads
96
Readme
Test MCP Server for MCP E2E Tests
A test MCP server used for E2E tests in the MCP Client. This originally was based off of the MCP Everything Server, but has been given new capabilities which the MCP Client handles.
Building
npm install
npm run buildRunning
STDIO Transport (Default)
npm start
# or
```node dist/index.js stdio
### SSE Transport
```bash
npm run start:sse
# or
node dist/index.js sseStreamable HTTP Transport
npm run start:streamableHttp
# or
node dist/index.js streamableHttpStreamable HTTP with OAuth2 Authentication
npm run start:streamableHttp:auth
# or
node dist/index.js streamableHttp --authThis transport requires OAuth2 Bearer token authentication for all MCP endpoints.
Available Tools
| Tool | Description |
|------|-------------|
| echo | Echoes back the input |
| add | Adds two numbers |
| longRunningOperation | Demonstrates progress updates |
| printEnv | Prints environment variables |
| sampleLLM | Samples from an LLM using MCP's sampling feature |
| getTinyImage | Returns a small test image |
| annotatedMessage | Demonstrates content annotations |
| getResourceReference | Returns a resource reference |
| getResourceLinks | Returns multiple resource links |
| structuredContent | Returns structured content with schema |
| zip | Creates a zip file from URLs |
| listRoots | Lists MCP roots (if client supports) |
| startElicitation | Demonstrates user input elicitation (if client supports) |
Available Prompts
| Prompt | Description |
|--------|-------------|
| simple_prompt | A prompt without arguments |
| complex_prompt | A prompt with temperature and style arguments |
| resource_prompt | A prompt that includes an embedded resource |
Resources
The server provides 100 static resources:
- Even-numbered resources contain plaintext
- Odd-numbered resources contain binary data
- Accessible via URI pattern:
test://static/resource/{id}
Project Structure
src/
├── index.ts # Main entry point with transport selection
├── logger.ts # Pino logger configuration
├── server/
│ └── index.ts # Server creation and request handler wiring
├── tools/
│ ├── index.ts # Tool registry, listing, and call routing
│ ├── types.ts # Tool type definitions
│ ├── constants.ts # Tool names enum and constants
│ └── definitions/ # Individual tool definitions
│ ├── echo.ts
│ ├── add.ts
│ └── ... # One file per tool
├── prompts/
│ └── index.ts # Prompt definitions and handlers
├── resources/
│ └── index.ts # Resource definitions and handlers
├── oauth/
│ ├── index.ts # OAuth module exports
│ ├── config.ts # OAuth configuration
│ ├── types.ts # OAuth type definitions
│ ├── stores.ts # In-memory token/client stores
│ ├── helpers.ts # Token generation utilities
│ ├── middleware.ts # Auth middleware
│ └── routes.ts # OAuth endpoints
└── transports/
├── stdio.ts # Standard I/O transport
├── sse.ts # Server-Sent Events transport
└── streamableHttpServer.ts # Streamable HTTP (with optional OAuth2)Scripts
| Command | Description |
|---------|-------------|
| npm run build | Compile TypeScript to JavaScript |
| npm run clean | Remove dist/ and build artifacts |
| npm run clean:all | Remove dist/, node_modules/, logs, and all generated files |
| npm run lint | Check for linting errors |
| npm run lint:fix | Fix auto-fixable linting errors |
| npm run format | Format code with Prettier |
| npm run format:check | Check if code is formatted |
| npm run watch | Watch mode for development |
| npm start | Start with STDIO transport |
| npm run start:stdio | Start with STDIO transport |
| npm run start:sse | Start with SSE transport |
| npm run start:streamableHttp | Start with Streamable HTTP transport |
| npm run start:streamableHttp:auth | Start with Streamable HTTP + OAuth2 |
| npm test | Run all tests |
| npm run test:watch | Run tests in watch mode |
| npm run test:unit | Run only unit tests |
| npm run test:e2e | Run only E2E tests |
| npm run test:coverage | Run tests with coverage report |
Testing
This project uses Vitest for testing, with both unit and end-to-end (E2E) tests.
Test Structure
src/
├── tools/
│ └── __tests__/ # Unit tests for tools
│ ├── echo.test.ts
│ ├── add.test.ts
│ ├── getTinyImage.test.ts
│ └── index.test.ts
├── prompts/
│ └── __tests__/ # Unit tests for prompts
│ └── index.test.ts
├── resources/
│ └── __tests__/ # Unit tests for resources
│ └── index.test.ts
└── oauth/
└── __tests__/ # Unit tests for OAuth
├── helpers.test.ts
└── middleware.test.ts
e2e/ # End-to-end tests
├── helpers/
│ └── server.ts # Test helpers for server management
├── stdio.test.ts # STDIO transport tests
├── sse.test.ts # SSE transport tests
├── streamableHttp.test.ts # Streamable HTTP transport tests
└── oauth.test.ts # OAuth2 flow testsRunning Tests
# Run all tests
npm test
# Run tests in watch mode (during development)
npm run test:watch
# Run only unit tests
npm run test:unit
# Run only E2E tests
npm run test:e2e
# Run tests with coverage report
npm run test:coverageUnit Tests
Unit tests verify individual components in isolation:
- Tools: Test handler outputs for each tool (echo, add, getTinyImage, etc.)
- Prompts: Test prompt generation and completion handling
- Resources: Test resource creation, listing, pagination, and reading
- OAuth: Test token generation, validation, and middleware
Example test:
import { describe, it, expect } from "vitest";
import { handler } from "../definitions/echo.js";
describe("echo tool", () => {
it("should echo the message back", async () => {
const result = await handler({ message: "hello" }, mockExtra, mockContext);
expect(result.content[0].text).toBe("Echo: hello");
});
});E2E Tests
E2E tests spin up the actual server and test real client-server communication:
- STDIO Transport: Tests using
StdioClientTransport - SSE Transport: Tests using
SSEClientTransport - Streamable HTTP Transport: Tests using
StreamableHTTPClientTransport - OAuth Flow: Tests full OAuth2 flows (client_credentials, authorization_code with PKCE, refresh_token)
E2E tests verify:
- Server startup and connection
- Tool listing and execution
- Prompt listing and retrieval
- Resource listing, pagination, and reading
- OAuth token issuance, introspection, and revocation
- Authenticated MCP requests
Prerequisites for Testing
Before running tests, ensure the project is built:
npm run buildE2E tests require the built dist/ files since they spawn server processes.
Configuration
Configure logging via environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| LOG_LEVEL | Log level (trace, debug, info, warn, error, fatal) | debug |
Component Loggers
The codebase uses child loggers for different components:
server- Core server operationstransport- Transport layer (stdio, sse, http)tools- Tool executionoauth- OAuth2 authentication
OAuth2 Configuration
Configure via environment variables (optional, defaults provided for testing):
OAUTH_CLIENT_ID- Client ID (default:mcp-client)OAUTH_CLIENT_SECRET- Client secret (default:mcp-secret)OAUTH_TOKEN_SECRET- Secret for signing tokens (change in production!)PORT- Server port (default:3001)
Supported Grant Types
| Grant Type | Description |
|------------|-------------|
| client_credentials | Server-to-server authentication |
| authorization_code | User authorization flow (with optional PKCE) |
| refresh_token | Refresh an expired access token |
Getting a Token (Client Credentials)
curl -X POST http://localhost:3001/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=mcp-client" \
-d "client_secret=mcp-secret"Authorization Code Flow (with PKCE)
- Generate PKCE values (client-side):
# Generate code_verifier (43-128 chars)
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')
# Generate code_challenge (S256)
CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl sha256 -binary | base64 | tr -d '=' | tr '/+' '_-')- Redirect user to authorization endpoint:
http://localhost:3001/oauth/authorize?
response_type=code&
client_id=mcp-client&
redirect_uri=http://localhost:3000/callback&
scope=mcp:read%20mcp:write&
state=random-state&
code_challenge=$CODE_CHALLENGE&
code_challenge_method=S256- Exchange code for tokens:
curl -X POST http://localhost:3001/oauth/token \
-d "grant_type=authorization_code" \
-d "code=<authorization-code>" \
-d "redirect_uri=http://localhost:3000/callback" \
-d "client_id=mcp-client" \
-d "code_verifier=$CODE_VERIFIER"Refreshing Tokens
curl -X POST http://localhost:3001/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=<your-refresh-token>" \
-d "client_id=mcp-client"Response (for authorization_code and refresh_token grants):
{
"access_token": "uuid-token.signature",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_uuid...",
"scope": "mcp:read mcp:write"
}Using the Token
Include the token in all MCP requests:
curl -X POST http://localhost:3001/mcp \
-H "Authorization: Bearer <your-access-token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'OAuth2 Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| /.well-known/oauth-authorization-server | GET | Authorization Server Metadata (RFC 8414) |
| /oauth/register | POST | Dynamic Client Registration (RFC 7591) |
| /oauth/authorize | GET | Authorization endpoint for authorization_code flow |
| /oauth/token | POST | Token endpoint (all grant types) |
| /oauth/introspect | POST | Token introspection (RFC 7662) |
| /oauth/revoke | POST | Token revocation (RFC 7009) |
| / | GET | Server info and endpoint documentation |
Authorization Server Metadata
Clients can discover OAuth2 configuration automatically:
curl http://localhost:3001/.well-known/oauth-authorization-serverResponse:
{
"issuer": "http://localhost:3001",
"authorization_endpoint": "http://localhost:3001/oauth/authorize",
"token_endpoint": "http://localhost:3001/oauth/token",
"registration_endpoint": "http://localhost:3001/oauth/register",
"introspection_endpoint": "http://localhost:3001/oauth/introspect",
"revocation_endpoint": "http://localhost:3001/oauth/revoke",
"grant_types_supported": ["client_credentials", "authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"],
"code_challenge_methods_supported": ["plain", "S256"],
"scopes_supported": ["mcp:read", "mcp:write"]
}Dynamic Client Registration
Register a new client dynamically:
curl -X POST http://localhost:3001/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"grant_types": ["authorization_code", "refresh_token"],
"redirect_uris": ["http://localhost:3000/callback"],
"scope": "mcp:read mcp:write"
}'Response:
{
"client_id": "client-abc123...",
"client_secret": "xyz789...",
"client_id_issued_at": 1234567890,
"client_secret_expires_at": 0,
"client_name": "My MCP Client",
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_post",
"scope": "mcp:read mcp:write"
}Use the returned client_id and client_secret to obtain access tokens.
Adding New Capabilities
Adding a New Tool
Each tool lives in its own file under src/tools/definitions/. To add a new tool:
Create a new file
src/tools/definitions/myNewTool.ts:import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import type { ToolDefinition, ToolHandler, ToolContext } from "../types.js"; // Tool name (exported for use in index.ts) export const name = "myNewTool"; // Schema for input validation const inputSchema = z.object({ param1: z.string().describe("Description of param1"), }); // Tool definition with description and JSON schema export const definition: ToolDefinition = { description: "Description of what the tool does", inputSchema: zodToJsonSchema(inputSchema), }; // Handler implementation export const handler: ToolHandler = async (args, extra, context) => { const { param1 } = inputSchema.parse(args); // Your implementation here return { content: [{ type: "text", text: `Result: ${param1}` }], }; };Register it in
src/tools/index.ts:import * as myNewTool from "./definitions/myNewTool.js"; // Add to tools array in getTools() { name: myNewTool.name, description: myNewTool.definition.description, inputSchema: myNewTool.definition.inputSchema, } // Add to handleToolCall() if (name === myNewTool.name) { return myNewTool.handler(args, extra, context); }
Adding a New Prompt
Add the prompt name to
src/prompts/index.ts:export enum PromptName { // ... existing prompts MY_PROMPT = "my_prompt", }Register it in
getPrompts()and implement ingetPrompt().
Adding a New Resource
Modify src/resources/index.ts to add new resource types or extend the resource generation logic.
