Skip to main content
Glama

Timezone MCP Server

by sam-artuso
CLAUDE.md20.2 kB
# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview This is a **timezone MCP server** built with NestJS and TypeScript that provides: 1. **MCP (Model Context Protocol)** endpoints - For LLM clients like Claude Desktop to auto-discover and use timezone tools and prompts (authenticated via OAuth) 2. **REST API** (development only) - Traditional HTTP endpoints with Swagger/OpenAPI documentation (unauthenticated, for testing) The server provides timezone operations (get regions, cities, current time) with ISO 8601 formatted timestamps. It exposes both **MCP tools** (callable functions) and **MCP prompts** (pre-built conversation starters) to guide LLM interactions. **Key Architecture Decisions:** - **Production**: Only MCP endpoints available, protected by Google OAuth + email allowlist - **Development**: MCP endpoints (authenticated) + REST API and Swagger (unauthenticated for easy testing) ## Development Commands ### Running the Server The application has **two distinct runtime modes** controlled by the `MCP_TRANSPORT` environment variable, and behavior changes based on `NODE_ENV`: 1. **HTTP mode - Development** (`MCP_TRANSPORT=http`, `NODE_ENV=development`): ```bash NODE_ENV=development pnpm dev # Development with hot reload NODE_ENV=development pnpm start # Development mode ``` - Starts HTTP server on port 3000 - Enables REST API with Swagger UI at `/api` (unauthenticated) - Enables MCP endpoint at `/mcp` (authenticated via Google OAuth + email allowlist) - Health check at `/health` (public) - MCP auth endpoints at `/auth/mcp/*` 2. **HTTP mode - Production** (`MCP_TRANSPORT=http`, `NODE_ENV=production`): ```bash NODE_ENV=production pnpm start:prod # Production (compiled) ``` - Starts HTTP server on port 3000 - **Only** MCP endpoint at `/mcp` (authenticated via Google OAuth + email allowlist) - Health check at `/health` (public) - MCP auth endpoints at `/auth/mcp/*` - **REST API and Swagger disabled** 3. **stdio mode** (`MCP_TRANSPORT=stdio`): ```bash MCP_TRANSPORT=stdio pnpm start ``` - No HTTP server - pure stdio JSON-RPC communication - Disables all logging to keep stdout clean - Used by Claude Desktop when configured as subprocess - No authentication in this mode ### Testing ```bash pnpm test # Run all tests (unit + e2e) pnpm test:watch # Watch mode pnpm test:cov # With coverage report pnpm test:ui # Vitest UI pnpm test:e2e # Only e2e tests pnpm typecheck # TypeScript type checking ``` **Single test file:** ```bash pnpm vitest src/auth/auth.service.spec.ts ``` **Single test case:** ```bash pnpm vitest -t "should generate JWT token" ``` ### Code Quality ```bash pnpm lint # ESLint check pnpm lint:fix # Auto-fix linting issues pnpm format # Format with Prettier pnpm format:check # Check formatting ``` ### Build & Deployment ```bash pnpm build # Compile TypeScript to dist/ pnpm deploy # Deploy to Google Cloud Run (uses scripts/deploy.sh) pnpm clean # Remove node_modules, dist, coverage ``` ### MCP Testing Tools ```bash pnpm mcp-inspector:stdio # Test MCP stdio transport locally pnpm mcp-inspector:http # Test MCP HTTP transport (requires server running on :3000) pnpm mcp-inspector:http:prod # Test deployed MCP endpoint ``` ### Utilities ```bash pnpm swagger # Open Swagger UI in browser pnpm swagger:prod # Open deployed Swagger UI pnpm deps:check # Check for dependency updates pnpm deps:update # Update to latest minor/patch versions pnpm deps:update:major # Update including major versions ``` ## Architecture ### Multi-Mode Bootstrap The application uses a **conditional bootstrap strategy** in `src/main.test-helper.ts`: - **stdio mode**: Creates `ApplicationContext` (no HTTP server, logging disabled, no auth) - **HTTP mode (development)**: Creates NestJS app with REST API, Swagger UI, MCP endpoint with auth - **HTTP mode (production)**: Creates NestJS app with ONLY MCP endpoint (authenticated), health check, and auth endpoints - REST API and Swagger are disabled The actual entry point (`src/main.ts`) delegates to `bootstrap()` from `main.test-helper.ts` for testability. The `TimezoneModule` uses a **dynamic module pattern** (`forRoot()`) to conditionally register the REST controller based on `NODE_ENV`, while always providing `TimezoneService` for MCP tools. ### Multi-Mode Architecture Pattern ``` ┌────────────────────────────────────────────────────────────────────┐ │ Timezone MCP Server │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ Development Production stdio Mode │ │ (NODE_ENV=dev) (NODE_ENV=prod) (MCP_TRANSPORT=stdio) │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌──────────────┐ ┌─────────┐ │ │ │ NestJS │ │ NestJS App │ │ MCP │ │ │ │┌────────┐│ │┌────────────┐│ │ stdio │ │ │ ││REST API││ ││/health ││ │ (JSON- │ │ │ ││+Swagger││ ││/auth/mcp/* ││ │ RPC) │ │ │ │└────────┘│ │└────────────┘│ └────┬────┘ │ │ │┌────────┐│ │┌────────────┐│ │ │ │ ││ /mcp ││ ││ /mcp ││ │ │ │ ││ (auth) ││ ││ (auth) ││ │ │ │ │└────────┘│ │└────────────┘│ │ │ │ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ │ └───────────────────┴─────────────────────┘ │ │ ↓ │ │ TimezoneService │ │ (Shared Business Logic) │ └───────────────────────────────────────────────────────────────────┘ ``` **Key insight**: All three modes share the same `TimezoneService`, ensuring consistent behavior. The MCP tools (`src/mcp-tools/tools/*.tool.ts`) wrap `TimezoneService` methods, and MCP prompts (`src/mcp-tools/prompts/*.prompt.ts`) provide conversation templates. Both are auto-discovered by `@rekog/mcp-nest`. - **Development**: Full feature set - REST API (unauthenticated) + MCP endpoint (authenticated) - **Production**: Security-focused - Only MCP endpoint (authenticated), REST API disabled - **stdio**: Claude Desktop integration - No HTTP, pure MCP protocol, no auth ### Module Structure - **AppModule** - Root module, imports all others, configures MCP transport and authentication - Imports `McpAuthModule` from `@rekog/mcp-nest` for OAuth authentication - Configures MCP module with `EmailAllowlistGuard` to protect MCP endpoints - Conditionally imports `TimezoneModule.forRoot()` with dynamic controller registration - **MCP Auth Components** (in `src/mcp-auth/`) - Email allowlist validation for MCP authentication - `AllowlistService` - Checks if user email is in `ALLOWED_EMAILS` env var - `EmailAllowlistGuard` - Extends `McpAuthJwtGuard` from `@rekog/mcp-nest`, adds email validation - Only protects MCP endpoints (`/mcp`), REST API is unauthenticated (dev only) - **TimezoneModule** - Core timezone business logic (uses dynamic module pattern) - `TimezoneService` - Uses `Intl.supportedValuesOf('timeZone')` (always provided) - `TimezoneController` - REST endpoints (only registered when `NODE_ENV=development`) - **McpToolsModule** - MCP tool and prompt definitions - Tools: - `GetRegionsTool` - Wraps `timezoneService.getRegions()` - `GetCitiesTool` - Wraps `timezoneService.getCitiesInRegion()` - `GetTimezoneInfoTool` - Wraps `timezoneService.getTimeInTimezone()` - Prompts: - `ExploreTimezonesPrompt` - Guides users to discover regions and cities - `GetCurrentTimePrompt` - Helps get current time in a timezone - `CompareTimezonesPrompt` - Guides timezone comparison ### MCP Tool Pattern MCP tools use the `@Tool()` decorator from `@rekog/mcp-nest`: ```typescript @Injectable() export class GetRegionsTool { constructor(private readonly timezoneService: TimezoneService) {} @Tool({ name: 'get_regions', description: 'Get a list of all available timezone regions', parameters: z.object({}), // Zod schema for validation }) async execute() { const regions = this.timezoneService.getRegions() return { content: [{ type: 'text', text: JSON.stringify({ regions, count: regions.length }) }], } } } ``` Tools are automatically registered with MCP server and exposed via JSON-RPC `tools/list` and `tools/call`. ### MCP Prompt Pattern MCP prompts use the `@Prompt()` decorator from `@rekog/mcp-nest` to provide pre-built conversation starters for LLM clients: ```typescript @Injectable() export class GetCurrentTimePrompt { @Prompt({ name: 'get_current_time', description: 'Get guidance on checking the current time in a specific timezone', parameters: z.object({ timezone: z.string().describe('The IANA timezone identifier'), }), }) async execute({ timezone }: { timezone: string }) { return { description: `Guidance for getting current time in ${timezone}`, messages: [ { role: 'user' as const, content: { type: 'text' as const, text: `What is the current time in ${timezone}?`, }, }, { role: 'assistant' as const, content: { type: 'text' as const, text: `I'll get the current time for ${timezone} using the get_timezone_info tool.`, }, }, ], } } } ``` Prompts are automatically registered with MCP server and exposed via JSON-RPC `prompts/list` and `prompts/get`. ### Authentication Architecture **MCP Authentication** (via `@rekog/mcp-nest`): - Uses `McpAuthModule` from `@rekog/mcp-nest` package - Provides OAuth2 authentication specifically designed for MCP protocol - Currently configured with Google OAuth provider - Authentication endpoints at `/auth/mcp/*` (e.g., `/auth/mcp/authorize`, `/auth/mcp/callback`, `/auth/mcp/token`) **OAuth2 Flow**: 1. MCP client initiates OAuth flow via `/auth/mcp/authorize` 2. Redirects to Google OAuth provider 3. After authentication, callback at `/auth/mcp/callback` 4. `McpAuthModule` generates JWT access token 5. Client uses token in MCP requests **Email Allowlist Protection**: - Custom `EmailAllowlistGuard` extends `McpAuthJwtGuard` from `@rekog/mcp-nest` - After JWT validation, checks if user's email is in `ALLOWED_EMAILS` env var - Only applied to MCP endpoints via `McpModule.forRoot({ guards: [EmailAllowlistGuard] })` - **REST API (development only) is unauthenticated** for easy testing - **stdio mode has NO authentication** (direct integration with Claude Desktop) **Security Model**: - **Production**: Only MCP endpoints exist, all authenticated with OAuth + email allowlist - **Development**: MCP endpoints authenticated, REST API unauthenticated (testing convenience) - **stdio mode**: No authentication (trusted local process) ## Environment Configuration Copy `example.env` to `.env` and configure: **Required**: - `NODE_ENV` - `development` or `production` (controls REST API availability) - `MCP_AUTH_CLIENT_ID` - Google OAuth client ID - `MCP_AUTH_CLIENT_SECRET` - Google OAuth client secret - `MCP_AUTH_JWT_SECRET` - Secret key for signing MCP JWT tokens - `MCP_AUTH_SERVER_URL` - Base URL for OAuth callbacks (e.g., `http://localhost:3000` or production URL) - `ALLOWED_EMAILS` - Comma-separated list of allowed email addresses for MCP access **Optional**: - `PORT` - HTTP server port (default: 3000) - `MCP_TRANSPORT` - `stdio` or `http` (default: http) - `JWT_EXPIRES_IN` - Token expiration (default: 3600, can use `3600s`, `1h`, `1d`) - `GCLOUD_PROJECT_ID`, `GCLOUD_REGION` - For Google Cloud deployment **Note**: The file is named `example.env` (not `.env.example`) for consistent syntax highlighting. **Runtime Modes**: - **Development** (`NODE_ENV=development`): REST API + Swagger (unauthenticated) + MCP endpoint (authenticated) - **Production** (`NODE_ENV=production`): Only MCP endpoint (authenticated) + health check - **stdio mode** (`MCP_TRANSPORT=stdio`): No HTTP server, pure MCP stdio communication, no auth ## Testing Strategy ### Test File Organization - **Unit tests**: `*.spec.ts` files next to source files - **E2E tests**: `test/*.e2e.spec.ts` directory - **Test helpers**: - `test/test-config.helper.ts` - Mock `ConfigService` for E2E tests - `src/main.test-helper.ts` - Exports `bootstrap()` for testing entry point ### ConfigService Mocking Due to `ConfigModule.forRoot({ isGlobal: true })`, some E2E tests override the provider: ```typescript const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(ConfigService) .useValue(createMockConfigService()) .compile() ``` ### Known Test Issues Some E2E tests fail due to ConfigModule global registration preventing proper mocking. These are pre-existing issues that don't affect production functionality. ## Git Hooks - **Pre-commit** (Husky + lint-staged): ESLint + Prettier on staged `*.ts` files - **Pre-push**: Runs `pnpm lint`, `pnpm typecheck`, `pnpm test:cov` ## Package Manager **pnpm only** - Enforced via `packageManager` field in `package.json` and Corepack. The exact version (10.18.3) is locked. Node.js version managed via `.node-version` file (requires fnm or nvm). ## Adding New MCP Tools 1. Create tool file in `src/mcp-tools/tools/my-tool.tool.ts` 2. Use `@Tool()` decorator with Zod schema for parameters 3. Inject `TimezoneService` or other dependencies via constructor 4. Return `{ content: [{ type: 'text', text: JSON.stringify(result) }] }` 5. Add to `McpToolsModule` providers and exports 6. Create corresponding `*.spec.ts` unit test ## Adding New MCP Prompts 1. Create prompt file in `src/mcp-tools/prompts/my-prompt.prompt.ts` 2. Use `@Prompt()` decorator with: - `name` - Unique identifier (e.g., "my_prompt") - `description` - What guidance this prompt provides - `parameters` - Zod schema with string types only (all optional or required) 3. Return object with: - `description` (optional) - Contextual description - `messages` - Array of user/assistant message exchanges 4. Each message must have: - `role`: `'user'` or `'assistant'` - `content`: `{ type: 'text', text: 'message content' }` 5. Add to `McpToolsModule` providers and exports 6. Create corresponding `*.spec.ts` unit test **Example prompts in this project:** - `ExploreTimezonesPrompt` - No parameters, guides general exploration - `GetCurrentTimePrompt` - Single `timezone` parameter - `CompareTimezonesPrompt` - Two parameters (`timezone1`, `timezone2`) ## Customizing MCP Authentication The project uses `@rekog/mcp-nest`'s built-in authentication system with Google OAuth. To customize: **Change OAuth Provider**: 1. In `src/app/app.module.ts`, replace `GoogleOAuthProvider` with `GitHubOAuthProvider` or `AzureADOAuthProvider` 2. Update environment variables in `example.env` and deployment config 3. Update Google Cloud secrets if deploying to Cloud Run **Customize Email Allowlist**: - Modify `src/mcp-auth/allowlist.service.ts` to implement different access control (e.g., role-based) - Update `src/mcp-auth/email-allowlist.guard.ts` validation logic - Keep the guard registered in `McpModule.forRoot({ guards: [...] })` **Disable Authentication** (not recommended for production): - Remove `EmailAllowlistGuard` from `McpModule.forRoot({ guards: [] })` - Remove `McpAuthModule` import from `AppModule` **Note**: The REST API in development mode is intentionally unauthenticated for ease of testing. Only MCP endpoints require authentication. ## Deployment The project deploys to Google Cloud Run via `scripts/deploy.sh`. ### Prerequisites **Before first deployment**, you must set up secrets in Google Cloud Secret Manager: ```bash # 1. Enable Secret Manager API (first time only) gcloud services enable secretmanager.googleapis.com # 2. Create required secrets for MCP authentication echo -n "$(openssl rand -hex 32)" | gcloud secrets create mcp-auth-jwt-secret --data-file=- echo -n "YOUR_GOOGLE_CLIENT_ID" | gcloud secrets create mcp-auth-client-id --data-file=- echo -n "YOUR_GOOGLE_CLIENT_SECRET" | gcloud secrets create mcp-auth-client-secret --data-file=- echo -n "user@example.com,admin@example.com" | gcloud secrets create allowed-emails --data-file=- ``` **Get Google OAuth credentials**: 1. Go to [Google Cloud Console → APIs & Credentials](https://console.cloud.google.com/apis/credentials) 2. Create OAuth 2.0 Client ID 3. Add authorized redirect URI: `https://your-service.a.run.app/auth/mcp/callback` 4. Copy Client ID and Client Secret to secrets ### Deployment Process The deployment script: - Builds Docker image - Pushes to Google Artifact Registry - Deploys to Cloud Run with: - **Non-sensitive env vars** set in `cloudbuild.yaml`: - `NODE_ENV=production` (disables REST API and Swagger) - `MCP_TRANSPORT=http` - `JWT_EXPIRES_IN=3600` - `MCP_AUTH_SERVER_URL` (production callback base URL) - **Sensitive values** from Google Secret Manager: - `MCP_AUTH_CLIENT_ID` (Google OAuth client ID) - `MCP_AUTH_CLIENT_SECRET` (Google OAuth client secret) - `MCP_AUTH_JWT_SECRET` (JWT signing secret) - `ALLOWED_EMAILS` (comma-separated email allowlist) - Saves production URL to `latest-prod-url.txt` **Important**: In production (`NODE_ENV=production`), only the following endpoints are available: - `/health` - Health check (public) - `/mcp` - MCP endpoint (authenticated) - `/auth/mcp/*` - OAuth authentication endpoints - **REST API and Swagger are disabled** ### Configuration Strategy **Environment-specific values:** - **Local (.env)**: - `NODE_ENV=development` (enables REST API + Swagger) - `MCP_AUTH_SERVER_URL=http://localhost:3000` - **Production (cloudbuild.yaml)**: - `NODE_ENV=production` (disables REST API + Swagger) - `MCP_AUTH_SERVER_URL=https://your-service.a.run.app` **OAuth callback URLs**: - Local: `http://localhost:3000/auth/mcp/callback` - Production: `https://your-service.a.run.app/auth/mcp/callback` - Both must be registered in Google Cloud Console OAuth credentials **Security model:** - Non-sensitive config → `cloudbuild.yaml` (e.g., `NODE_ENV`, `MCP_TRANSPORT`, `MCP_AUTH_SERVER_URL`) - Sensitive secrets → Google Secret Manager (e.g., `MCP_AUTH_JWT_SECRET`, `MCP_AUTH_CLIENT_SECRET`, `ALLOWED_EMAILS`)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sam-artuso/demo-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server