skeleton-mcp
by LesterAJohn
README.md
# skeleton-mcp
Node.js MCP skeleton with:
- Vault-backed secret management
- Postgres-backed configuration management
- Security and operational defaults for production-style patterns
## Purpose
This repository is a starter template for building an MCP server that needs:
- Multi-user support.
- Secret reads and writes through Vault
- Config reads and writes through Postgres
- Tool-level authorization for mutating operations
- Redacted tool output by default
- Basic reliability controls for external secret writes
Design requirements:
- Secrets are persistent in Vault.
- Configuration is persistent in Postgres.
- User-scoped behavior is mandatory, with default-user fallback where supported.
## Skeleton Architecture
Runtime flow:
1. [src/index.js](src/index.js) boots the app, creates services, and connects MCP stdio transport.
2. [src/config/env.js](src/config/env.js) loads and validates environment configuration.
3. [src/services/configStore.js](src/services/configStore.js) handles Postgres persistence.
4. [src/services/vault.js](src/services/vault.js) handles Vault operations and write retry queue.
5. [src/services/security.js](src/services/security.js) redacts sensitive fields.
6. [src/mcp/server.js](src/mcp/server.js) registers MCP tools and applies auth/error wrappers.
7. [src/http/server.js](src/http/server.js) exposes MCP over HTTP with auth, limits, and access logs.
8. [src/http/index.js](src/http/index.js) boots the dedicated HTTP MCP process.
9. [src/start-both.js](src/start-both.js) starts stdio and HTTP as separate child processes.
Local infrastructure:
- [docker-compose.yml](docker-compose.yml) runs Postgres and Vault for local development.
- [docker-compose.external.yml](docker-compose.external.yml) runs only the MCP app against external Postgres and Vault services.
- [initdb/001_config.sh](initdb/001_config.sh) creates and seeds the app-prefixed config table.
## Registering The MCP Server
This repository can be registered with MCP clients in either stdio mode or HTTP mode.
For local development, stdio is the simplest option:
```json
{
"mcpServers": {
"skeleton-mcp": {
"command": "npm",
"args": ["run", "start:stdio"],
"cwd": "/Users/lesterjohn/Documents/GitHub/skeleton-mcp"
}
}
}
```
For HTTP-capable clients, use `npm run start:http` and point the client at the `/mcp` endpoint.
### Codex
Use stdio for Codex unless you specifically need HTTP. Add the server to your Codex MCP configuration using the local workspace path:
- Config file: `~/.codex/config.toml`
- Transport: stdio
```json
{
"mcpServers": {
"skeleton-mcp": {
"command": "npm",
"args": ["run", "start:stdio"],
"cwd": "/Users/lesterjohn/Documents/GitHub/skeleton-mcp"
}
}
}
```
If you prefer HTTP, run `npm run start:http` in this repository and configure Codex to send MCP requests to `http://127.0.0.1:3000/mcp`.
### VS Code
Use stdio in VS Code for local workspace access, or HTTP if your setup routes MCP servers over a local endpoint:
- Config file: `.vscode/mcp.json`
- Transport: stdio or HTTP
```json
{
"command": "npm",
"args": ["run", "start:stdio"],
"cwd": "/Users/lesterjohn/Documents/GitHub/skeleton-mcp"
}
```
If your VS Code setup uses HTTP transport, point it at `http://127.0.0.1:3000/mcp` after starting `npm run start:http`.
### Claude
For Claude Desktop, use stdio and add an MCP server entry that launches the process from this repository:
- Config file: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Transport: stdio
```json
{
"mcpServers": {
"skeleton-mcp": {
"command": "npm",
"args": ["run", "start:stdio"],
"cwd": "/Users/lesterjohn/Documents/GitHub/skeleton-mcp"
}
}
}
```
If you are using a Claude setup that supports MCP over HTTP, point it at `http://127.0.0.1:3000/mcp`.
## Tool Catalog
All tools return MCP text content containing JSON. Success payloads follow this shape:
```json
{
"ok": true,
"status": 200,
"data": {}
}
```
Errors follow this shape and set MCP `isError=true`:
```json
{
"ok": false,
"status": 401,
"error": "Unauthorized: invalid authorizationKey for mutating API operation"
}
```
If `MCP_ADMIN_AUTH_KEY` is configured, mutating tools (and mutating `service_api_request` calls) require `authorizationKey`.
### service_query_suggestion
- Category: advisory (read-only)
- Risk: low
- Use when: you want schema discovery for all MCP tools and recommendations on what tool sequence to run for an intent/workflow
- Do not use when: you already know the exact specialized tool and validated endpoint path
- Required permissions/prerequisites: none
- Environment behavior:
- Detects whether a workflow is mutating from `operationType` or `method`
- Reports whether `authorizationKey` is required for the suggested mutating call when `MCP_ADMIN_AUTH_KEY` is configured
- Parameters:
- `intent` (optional string)
- `operationType` (optional enum: `discover|read|mutate|suspend_logging|resume_logging|get_drive_gpx|scoped`)
- `method` (optional string, normalized to uppercase)
- `path` (optional string, normalized to leading slash)
- `includeExamples` (optional boolean, default `true`)
- `includeToolSchemas` (optional boolean, default `true`)
- Expected response shape:
- `data.summary`: intent + mutating/auth flags
- `data.recommendedOrder`: ordered tool recommendations with reasons
- `data.safetyChecks`: checklist for safer execution
- `data.toolSchemas`: schema-like catalog for all operational tools (unless disabled)
- `data.examples`: baseline read/mutate examples (unless disabled)
- Common failures:
- `500` for unexpected internal recommendation errors
- Recommended tools:
- Prerequisite: none
- Follow-up: any tool listed in `data.recommendedOrder`
- Example:
```json
{
"name": "service_query_suggestion",
"arguments": {
"intent": "discover endpoints then suspend logging for car 42",
"operationType": "suspend_logging",
"method": "PUT",
"path": "/api/car/42/logging/suspend"
}
}
```
#### Recommended Playbooks
| Intent | Suggested tool sequence | Notes |
| --- | --- | --- |
| Verify runtime and service connectivity | `service_connection_info` -> `service_health_check` | Baseline check before any operational request. |
| Discover supported routes before custom request | `service_connection_info` -> `service_health_check` -> `service_list_endpoints` -> `service_api_request` | Use discovered paths to reduce schema/path mistakes. |
| Suspend logging for a car | `service_connection_info` -> `service_health_check` -> `service_suspend_logging` | If `MCP_ADMIN_AUTH_KEY` is set, pass `authorizationKey`. |
| Resume logging for a car | `service_connection_info` -> `service_health_check` -> `service_resume_logging` | Prefer specialized tool over generic mutate calls. |
| Export drive GPX | `service_connection_info` -> `service_health_check` -> `service_get_drive_gpx` | Dedicated GPX path and payload handling. |
| Execute a generic read request | `service_connection_info` -> `service_health_check` -> `service_list_endpoints` -> `service_api_request` (`GET`) | Safer for non-destructive endpoint exploration. |
| Execute a generic mutating request | `service_connection_info` -> `service_health_check` -> `service_list_endpoints` -> `service_api_request` (`POST|PUT|PATCH|DELETE`) | Confirm method/path/body first; provide `authorizationKey` when required. |
| Inspect user/app scope metadata | `service_connection_info` -> `service_scope_info` -> `service_api_request` | Use scope details for app/user-aware workflows. |
### service_connection_info
- Category: read-only
- Risk: low
- Use when: you need runtime MCP + target-service connection metadata (server name/version, auth-gate status, scope model, service base URL/timeout)
- Do not use when: you need health verification or endpoint discovery; prefer `service_health_check` or `service_list_endpoints`
- Required permissions/prerequisites: none
- Environment behavior: reports current process config (`APP_NAME`, `MCP_CONFIG_DEFAULT_USER_ID`, and admin-auth configured state)
- Parameters: none
- Expected response shape:
- `data.server`: server metadata and `scopeModel`
- `data.service`: service client connection info
- Common failures:
- `500` if service client metadata retrieval fails
- Recommended tools:
- Prerequisite: none
- Follow-up: `service_health_check`, `service_list_endpoints`
- Example:
```json
{
"name": "service_connection_info",
"arguments": {}
}
```
### service_scope_info
- Category: read-only
- Risk: low
- Use when: you need app/user scoping details used for Postgres and Vault paths before making scoped calls
- Do not use when: you only need default scope metadata; `service_connection_info` already includes default scope model
- Required permissions/prerequisites: none
- Environment behavior: defaults `userId` to `MCP_CONFIG_DEFAULT_USER_ID` when omitted
- Parameters:
- `userId` (optional string, non-empty)
- Expected response shape:
- `data.appName`
- `data.userId`
- `data.userIdPathSegment`
- `data.postgres.tableName`, `data.postgres.primaryKey`, `data.postgres.scope`
- `data.vault.tokenIndexPath`, `data.vault.scope`
- Common failures:
- `500` for unexpected normalization/path construction errors
- Recommended tools:
- Prerequisite: none
- Follow-up: `service_api_request` (for scoped API calls)
- Example:
```json
{
"name": "service_scope_info",
"arguments": {
"userId": "default"
}
}
```
### service_list_endpoints
- Category: read-only
- Risk: low
- Use when: you want a safe discovery list of implemented/documented target endpoints
- Do not use when: you need endpoint liveness; use `service_health_check`
- Required permissions/prerequisites: none
- Environment behavior: output depends on service adapter implementation bundled with current build
- Parameters: none
- Expected response shape:
- `data.endpoints`: adapter-provided endpoint list
- Common failures:
- `500` if endpoint catalog cannot be produced
- Recommended tools:
- Prerequisite: `service_connection_info`
- Follow-up: `service_api_request`, `service_health_check`
- Example:
```json
{
"name": "service_list_endpoints",
"arguments": {}
}
```
### service_health_check
- Category: read-only
- Risk: low
- Use when: you need reachability/health validation before operational calls
- Do not use when: you need to retrieve business data; use specific service tools
- Required permissions/prerequisites: network path to target service must be available
- Environment behavior: uses configured service base URL and auth mode from runtime env
- Parameters: none
- Expected response shape:
- `data`: adapter-specific health payload
- Common failures:
- `5xx` if target service is down/unreachable
- `401/403` when upstream service credentials are invalid
- Recommended tools:
- Prerequisite: `service_connection_info`
- Follow-up: `service_list_endpoints`, `service_api_request`
- Example:
```json
{
"name": "service_health_check",
"arguments": {}
}
```
### service_suspend_logging
- Category: mutating
- Risk: high
- Use when: you intentionally need to disable logging for a specific car resource
- Do not use when: you are exploring state only; use read-only tools first
- Required permissions/prerequisites:
- If `MCP_ADMIN_AUTH_KEY` is set, `authorizationKey` must match
- Caller should validate target id and operational impact
- Environment behavior: authorization enforcement is env-driven (`MCP_ADMIN_AUTH_KEY`)
- Parameters:
- `carId` (required, non-empty string or positive integer)
- `authorizationKey` (optional unless admin key is configured)
- Expected response shape:
- `data`: adapter-specific suspend result
- Common failures:
- `401` invalid/missing `authorizationKey` when required
- `404` car id not found
- `409` invalid state transition (already suspended)
- `5xx` upstream service failure
- Safety warning: this changes operational behavior; confirm rollback path before invoking
- Recommended tools:
- Prerequisite: `service_health_check`, `service_list_endpoints`
- Follow-up: `service_resume_logging`, `service_api_request` (state verification)
- Example:
```json
{
"name": "service_suspend_logging",
"arguments": {
"carId": 42,
"authorizationKey": "<admin-key-if-required>"
}
}
```
### service_resume_logging
- Category: mutating
- Risk: high
- Use when: you need to restore logging after a prior suspension
- Do not use when: you only need status checks
- Required permissions/prerequisites:
- If `MCP_ADMIN_AUTH_KEY` is set, `authorizationKey` must match
- Logging was previously suspended for the target resource
- Environment behavior: authorization enforcement is env-driven (`MCP_ADMIN_AUTH_KEY`)
- Parameters:
- `carId` (required, non-empty string or positive integer)
- `authorizationKey` (optional unless admin key is configured)
- Expected response shape:
- `data`: adapter-specific resume result
- Common failures:
- `401` invalid/missing `authorizationKey` when required
- `404` car id not found
- `409` invalid state transition (not suspended)
- `5xx` upstream service failure
- Safety warning: operational state change; coordinate with incident/runbook procedures
- Recommended tools:
- Prerequisite: `service_health_check`
- Follow-up: `service_api_request` (state verification)
- Example:
```json
{
"name": "service_resume_logging",
"arguments": {
"carId": "42",
"authorizationKey": "<admin-key-if-required>"
}
}
```
### service_get_drive_gpx
- Category: read-only
- Risk: medium (may contain sensitive location/history data)
- Use when: you need a GPX export for a specific drive id
- Do not use when: you only need summary metadata; use a lighter endpoint via `service_api_request` if available
- Required permissions/prerequisites: drive id must exist and caller must be permitted by upstream service
- Environment behavior: uses configured service auth and base URL
- Parameters:
- `driveId` (required, non-empty string or positive integer)
- Expected response shape:
- `data`: adapter-specific GPX payload/metadata
- Common failures:
- `404` drive id not found
- `401/403` upstream authorization failure
- `5xx` upstream service failure
- Recommended tools:
- Prerequisite: `service_health_check`
- Follow-up: `service_api_request` for related resource lookups
- Example:
```json
{
"name": "service_get_drive_gpx",
"arguments": {
"driveId": 123
}
}
```
### service_api_request
- Category: read-only or mutating (depends on `method`)
- Risk: variable; high for `POST|PUT|PATCH|DELETE`
- Use when: you need flexible access to supported target-service endpoints not covered by specialized tools
- Do not use when: a specialized tool exists (`service_suspend_logging`, `service_resume_logging`, `service_get_drive_gpx`) because those provide clearer intent and safer contracts
- Required permissions/prerequisites:
- Mutating methods require `authorizationKey` if `MCP_ADMIN_AUTH_KEY` is set
- `path` must target valid adapter-supported route behavior
- Environment behavior:
- `method` is normalized to uppercase
- `path` is normalized to leading slash format
- auth and host safeguards are enforced by adapter
- Parameters:
- `method` (required string, e.g. `GET`, `POST`)
- `path` (required string; `/`-prefixed normalization applied)
- `query` (optional object: string keys, scalar values string|number|boolean)
- `body` (optional JSON value)
- `headers` (optional object: string keys and string values)
- `authorizationKey` (optional unless mutating call requires it)
- Expected response shape:
- `data`: adapter HTTP response payload
- Common failures:
- `400` invalid path/query/body for upstream API
- `401` invalid/missing `authorizationKey` for required mutating call
- `403` upstream access denied
- `404` endpoint/resource not found
- `429` upstream rate limiting
- `5xx` upstream/transport failure
- Safety warning: treat mutating methods as destructive-capable operations; verify target path, method, and body before invocation
- Recommended tools:
- Prerequisite: `service_list_endpoints`, `service_health_check`
- Follow-up: specialized read tools for validation, or another `service_api_request` GET for post-change verification
- Examples:
```json
{
"name": "service_api_request",
"arguments": {
"method": "GET",
"path": "/api/car/42"
}
}
```
```json
{
"name": "service_api_request",
"arguments": {
"method": "PATCH",
"path": "/api/car/42",
"body": {
"nickname": "track-ready"
},
"authorizationKey": "<admin-key-if-required>"
}
}
```
## Security Behavior
- Sensitive fields are redacted unless MCP_ALLOW_SENSITIVE_OUTPUT=true.
- Mutating operations can be access controlled with MCP_ADMIN_AUTH_KEY.
- Vault write operations are serialized through an internal queue and retried with exponential backoff.
## Environment Variables
Core:
- APP_NAME
- MCP_SERVER_NAME
- MCP_SERVER_VERSION
- MCP_ALLOW_SENSITIVE_OUTPUT
- MCP_ADMIN_AUTH_KEY
- MCP_TRANSPORT_MODE (`stdio`, `http`, or `both`)
- MCP_CONFIG_DEFAULT_USER_ID
- MCP_TOKEN_ROTATION_DEFAULT_INTERVAL_MS
- MCP_TOKEN_ROTATION_USER_INTERVAL_CONFIG_KEY
- MCP_VAULT_AGENT_AUTH_MODE_CONFIG_KEY
- MCP_VAULT_AGENT_TOKEN_FILE_PATH_CONFIG_KEY
- MCP_VAULT_AGENT_LISTENER_ADDR_CONFIG_KEY
HTTP transport:
- MCP_HTTP_HOST
- MCP_HTTP_PORT
- MCP_HTTP_PATH
- MCP_HTTP_HEALTH_PATH
- MCP_HTTP_AUTH_MODE (`token`, `oauth2`, `both`)
- MCP_HTTP_TOKEN_SOURCE (`vault`, `env`)
- MCP_HTTP_AUTH_TOKENS (comma-separated bearer tokens)
- MCP_HTTP_TRUST_PROXY
- MCP_HTTP_ALLOWED_ORIGINS (comma-separated)
- MCP_HTTP_ALLOWED_IPS (comma-separated)
- MCP_HTTP_MAX_BODY_BYTES
- MCP_HTTP_RATE_LIMIT_WINDOW_MS
- MCP_HTTP_RATE_LIMIT_MAX_REQUESTS
- MCP_HTTP_VAULT_TOKEN_INDEX_PATH
- MCP_HTTP_VAULT_TOKEN_DEFAULT_USER_ID
- MCP_HTTP_VAULT_TOKEN_REQUIRED_SCOPES
- MCP_HTTP_VAULT_TOKEN_REQUIRED_AUDIENCE
- MCP_HTTP_VAULT_TOKEN_CACHE_TTL_MS
- MCP_HTTP_OAUTH2_INTROSPECTION_URL
- MCP_HTTP_OAUTH2_CLIENT_ID
- MCP_HTTP_OAUTH2_CLIENT_SECRET
- MCP_HTTP_OAUTH2_REQUIRED_SCOPES
- MCP_HTTP_OAUTH2_REQUIRED_AUDIENCE
- MCP_HTTP_OAUTH2_TIMEOUT_MS
- MCP_HTTP_OAUTH2_CACHE_TTL_MS
- MCP_HTTP_TLS_ENABLED
- MCP_HTTP_TLS_CERT_PATH
- MCP_HTTP_TLS_KEY_PATH
Postgres:
- POSTGRES_HOST
- POSTGRES_PORT
- POSTGRES_DB
- POSTGRES_USER
- POSTGRES_PASSWORD
Postgres config model:
- Configuration data is app-scoped in `${APP_NAME}_config` and user-scoped with composite key `(user_id, key)`.
- MCP config tools accept optional `userId`; when omitted, `MCP_CONFIG_DEFAULT_USER_ID` is used.
- Seed records include:
- `default/sample.feature`
- `default/app.defaults` for future default parameters.
- `default/token.rotation.intervalMs`
- `default/vault.agent.auth.mode`
- `default/vault.agent.tokenFilePath`
- `default/vault.agent.listener.addr`
Vault:
- VAULT_ADDR
- VAULT_TOKEN
- VAULT_AGENT_ENABLED
- VAULT_AGENT_AUTH_MODE (`none`, `file`, `listener`, `both`)
- VAULT_AGENT_TOKEN_FILE_PATH
- VAULT_AGENT_LISTENER_ENABLED
- VAULT_AGENT_LISTENER_ADDR
- VAULT_UNSEAL_KEY
- VAULT_KV_MOUNT
- VAULT_WRITE_RETRY_ATTEMPTS
- VAULT_WRITE_RETRY_BASE_DELAY_MS
- VAULT_WRITE_RETRY_MAX_DELAY_MS
Naming defaults:
- `APP_NAME` defaults to `skeleton`.
- The Postgres config table defaults to `${APP_NAME}_config`.
- The Vault token index path defaults to `${APP_NAME}/users/${MCP_HTTP_VAULT_TOKEN_DEFAULT_USER_ID}/http/auth/token-index`.
- Set only `APP_NAME` to rename the app-scoped Vault/Postgres schema across local and external stores.
Reference values are in [.env.example](.env.example).
## Quick Start
1. Install dependencies.
2. Copy .env.example to .env.
3. Start local services with docker compose up -d.
4. Resolve the managed unseal key: `npm run vault:unseal-key -- --json`.
5. Initialize and unseal local Vault (first run):
```bash
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 skeleton-mcp-vault vault operator init -key-shares=1 -key-threshold=1 -format=json
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 skeleton-mcp-vault vault operator unseal <unseal_key_from_init_or_env>
```
6. Seed a test secret in Vault.
7. Start the MCP server with npm start.
8. Run tests with npm test.
## External Services Mode
Use this mode when Vault and Postgres are already managed outside this repository.
Required environment variables:
- `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`
- `VAULT_ADDR`, `VAULT_TOKEN`
Run the app-only compose stack:
```bash
docker compose -f docker-compose.external.yml up -d
```
Notes:
- The app still uses Vault for secrets and Postgres for config.
- The external stack is the same MCP HTTP container, but it skips local Postgres/Vault containers.
- If you keep Vault sealed, the app will still require whatever unseal process your external Vault uses.
### Test Coverage Notes
Current automation includes listener-related coverage for Vault Agent runtime resolution.
- [tests/vault-agent-runtime.test.js](tests/vault-agent-runtime.test.js) validates:
- listener mode resolution from Postgres defaults
- both mode resolution (listener + file)
- fallback to environment values when database mode is invalid
Transport scripts:
```bash
# stdio only (default)
npm run start:stdio
# HTTP transport only
npm run start:http
# run stdio + HTTP as two processes
npm run start:both
```
## HTTP MCP Endpoint
Default endpoint values:
- MCP URL: `http://127.0.0.1:3000/mcp`
- Health URL: `http://127.0.0.1:3000/healthz`
HTTP transport security controls:
- Every `/mcp` request requires `Authorization: Bearer <token>`.
- For `MCP_HTTP_AUTH_MODE=token`, set `MCP_HTTP_TOKEN_SOURCE=vault` to validate tokens from Vault.
- For `MCP_HTTP_AUTH_MODE=oauth2`, bearer tokens are validated by OAuth2 introspection.
- For `MCP_HTTP_AUTH_MODE=both`, either token strategy can authorize requests.
- Mutating tools still require `authorizationKey` when `MCP_ADMIN_AUTH_KEY` is set.
- Request limits are enforced with:
- `MCP_HTTP_MAX_BODY_BYTES`
- `MCP_HTTP_RATE_LIMIT_WINDOW_MS`
- `MCP_HTTP_RATE_LIMIT_MAX_REQUESTS`
- Optional network restrictions:
- `MCP_HTTP_ALLOWED_ORIGINS`
- `MCP_HTTP_ALLOWED_IPS`
### Vault Multi-User Token Model
Store HTTP bearer tokens in Vault at `MCP_HTTP_VAULT_TOKEN_INDEX_PATH`.
If unset, seeding tools default to `${APP_NAME}/users/<user_id>/http/auth/token-index`.
Default-user fallback behavior:
- `MCP_HTTP_VAULT_TOKEN_DEFAULT_USER_ID` defaults to `default`.
- If no non-default users exist in the token index, the default user is always used as fallback.
Supported index shape:
```json
{
"tokens": {
"<sha256(token)>": {
"userId": "user-123",
"tokenId": "tok-123",
"active": true,
"scopes": ["mcp:invoke", "mcp:read"],
"audience": ["codex", "claude"],
"expiresAt": "2026-12-31T23:59:59Z"
}
}
}
```
Notes:
- Store only token hashes in Vault index data, never plaintext tokens.
- `MCP_HTTP_VAULT_TOKEN_REQUIRED_SCOPES` and `MCP_HTTP_VAULT_TOKEN_REQUIRED_AUDIENCE` enforce policy checks.
- This keeps secrets in Vault under the app-prefixed root while configuration remains in the app-prefixed Postgres table.
### Vault HTTP Token Seeding
Use the helper script to generate an opaque bearer token and store it in the Vault user token structure:
```bash
npm run vault:seed-http-token -- --user-id default --json
```
Useful options:
- `--user-id <id>`: Vault user to seed.
- `--token-id <id>`: Optional token id stored with the entry.
- `--scopes <list>`: Comma or space separated scopes.
- `--audience <list>`: Comma or space separated audience values.
- `--expires-at <value>`: Optional ISO timestamp or unix seconds.
- `--path <vault-path>`: Override the token index path.
The script writes the token record under the app-prefixed user structure and mirrors the token in the top-level token map for compatibility.
If you need to reseed a user, run the script again with the same `--user-id` and a new `--token-id`.
### Vault OAuth Token Seeding
Use the helper script to store a provided OAuth access token in the Vault user token structure:
```bash
npm run vault:seed-oauth-token -- --token "$OAUTH_ACCESS_TOKEN" --user-id default --json
```
Useful options:
- `--token <value>`: OAuth access token to seed.
- `--user-id <id>`: Vault user to seed.
- `--token-id <id>`: Optional token id stored with the entry.
- `--scopes <list>`: Comma or space separated scopes.
- `--audience <list>`: Comma or space separated audience values.
- `--expires-at <value>`: Optional ISO timestamp or unix seconds.
- `--path <vault-path>`: Override the token index path.
The script stores the provided token under the app-prefixed user structure, keeps the top-level token map aligned, and marks the entry as `oauth2` in Vault metadata.
### MCP Tool
The same capability is exposed as an MCP tool for controlled setup workflows:
- `vault_seed_http_token`: generate a bearer token and store it in the Vault HTTP token index for a user.
- `vault_seed_oauth_token`: store a provided OAuth access token in the Vault HTTP token index for a user.
For both tools, include app/user scope in requests so clients can reason about target storage:
- `appName` determines app-level namespace defaults.
- `userId` determines user-level namespace under `${APP_NAME}/users/<user_id>/...`.
The tool requires `authorizationKey` when `MCP_ADMIN_AUTH_KEY` is configured.
### Vault Token Lifecycle MCP Tools
The skeleton exposes node-vault token lifecycle methods as MCP tools:
- `token_lookup_self` -> `tokenLookupSelf`
- `token_renew_self` -> `tokenRenewSelf`
- `token_create` -> `tokenCreate`
- `token_revoke` -> `tokenRevoke`
- `token_revoke_self` -> `tokenRevokeSelf`
These tools are intended for controlled operational usage and are guarded by admin authorization when `MCP_ADMIN_AUTH_KEY` is configured.
### Vault Agent Auto-Auth and Token Renewal
Vault Agent can own token auth/renewal while this service reads the sink token file.
- Enable with `VAULT_AGENT_ENABLED=true`
- Choose auth mode with `VAULT_AGENT_AUTH_MODE`:
- `file`: use Vault Agent token sink file
- `listener`: use Vault Agent listener endpoint
- `both`: enable listener operations and file-based token read workflows
- Configure sink file path with `VAULT_AGENT_TOKEN_FILE_PATH`
- Enable listener with `VAULT_AGENT_LISTENER_ENABLED=true`
- Configure listener with `VAULT_AGENT_LISTENER_ADDR`
- Use `vault_agent_token_read` when application workflows need token sink visibility
When Vault Agent mode is enabled:
- File mode refreshes token state from the configured token sink path.
- Listener mode routes Vault operations through the configured Vault Agent listener.
- Both mode supports listener operations and token sink read workflows.
### Option 3: Postgres-Backed Non-Secret Vault Agent Settings
This skeleton supports storing non-secret Vault Agent runtime pointers/settings in Postgres while keeping token material in Vault.
- Runtime settings are read from default user config scope (`MCP_CONFIG_DEFAULT_USER_ID`).
- Key names are configurable with:
- `MCP_VAULT_AGENT_AUTH_MODE_CONFIG_KEY`
- `MCP_VAULT_AGENT_TOKEN_FILE_PATH_CONFIG_KEY`
- `MCP_VAULT_AGENT_LISTENER_ADDR_CONFIG_KEY`
- Recommended values in Postgres:
- `vault.agent.auth.mode`
- `vault.agent.tokenFilePath`
- `vault.agent.listener.addr`
### Rotation Time Configuration
Rotation interval supports both global defaults and user-scoped overrides:
- Global default env variable: `MCP_TOKEN_ROTATION_DEFAULT_INTERVAL_MS`
- User-scoped config key name: `MCP_TOKEN_ROTATION_USER_INTERVAL_CONFIG_KEY` (default `token.rotation.intervalMs`)
Effective value resolution is:
1. User-scoped Postgres config (`userId` + key)
2. Default user Postgres config (`default` + key)
3. Global env default
Use `token_rotation_config` tool to inspect the resolved rotation interval for a user scope.
Minimal remote call example:
```bash
curl -i http://127.0.0.1:3000/mcp \
-H "Authorization: Bearer replace-me-token" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "client", "version": "1.0.0" }
}
}'
```
## HTTPS Deployment Choice
This repository uses a reverse proxy (recommended): terminate TLS at a reverse proxy or load balancer.
- Keep this app on internal HTTP.
- Enforce HTTPS, client allowlists, and edge-level controls at the proxy/LB.
- Forward traffic to `MCP_HTTP_HOST:MCP_HTTP_PORT`.
- Keep `MCP_HTTP_TLS_ENABLED=false` in this process mode.
Popular patterns include Nginx, Traefik, Envoy, ALB/NLB, or Cloudflare Tunnel in front of `/mcp`.
## Vault Production Migration (Raft)
The repository now includes a Vault production migration scaffold under [vault-production](vault-production):
- [vault-production/config/vault.hcl](vault-production/config/vault.hcl): Raft-backed Vault server configuration.
- [vault-production/docker-compose.vault-prod.yml](vault-production/docker-compose.vault-prod.yml): Compose definition for Vault in server mode (non-dev).
- [vault-production/scripts/convert-dev-to-prod.sh](vault-production/scripts/convert-dev-to-prod.sh): Script to export dev KV data, start Raft Vault, initialize/unseal, and import secrets.
- [vault-production/scripts/bootstrap-post-conversion.sh](vault-production/scripts/bootstrap-post-conversion.sh): Script to enable audit, write policy, configure AppRole, and emit service credentials.
- [scripts/vault-unseal-key.js](scripts/vault-unseal-key.js): Script to resolve unseal key from `VAULT_UNSEAL_KEY` or `src/config/vault.unseal.key.json`.
- [vault-production/README.md](vault-production/README.md): Detailed migration notes and options.
Managed unseal key flow:
- `VAULT_UNSEAL_KEY` is optional and can be injected at runtime.
- If `VAULT_UNSEAL_KEY` is not set, `npm run vault:unseal-key` reads `src/config/vault.unseal.key.json`.
- If the key file is missing or empty, a 24-character key is generated and saved to `src/config/vault.unseal.key.json`.
- Both compose stacks run a one-shot `vault-unseal-key-init` service before Vault startup to ensure key material exists.
Run the conversion:
```bash
bash vault-production/scripts/convert-dev-to-prod.sh --init-keys-out vault-production/backups/vault-init.json
```
Common options:
```bash
# Use a different KV mount
bash vault-production/scripts/convert-dev-to-prod.sh --mount secret
# Migrate infra only (skip data movement)
bash vault-production/scripts/convert-dev-to-prod.sh --skip-export --skip-import
# Use when Raft Vault is already initialized
bash vault-production/scripts/convert-dev-to-prod.sh --skip-init
# Explicitly set key file path used by convert script
bash vault-production/scripts/convert-dev-to-prod.sh --unseal-key-path src/config/vault.unseal.key.json
# Post-conversion hardening bootstrap (audit, policy, AppRole)
bash vault-production/scripts/bootstrap-post-conversion.sh --vault-token <root_or_admin_token>
# CI-friendly machine output
bash vault-production/scripts/bootstrap-post-conversion.sh \
--vault-token <root_or_admin_token> \
--output json
```
## VS Code Agent Structure
This repository includes a project agent structure for adapting the skeleton to additional service-backed MCP implementations:
- [agent/README.md](agent/README.md): Overview of agent assets.
- [agent/playbooks/service-onboarding.md](agent/playbooks/service-onboarding.md): Step-by-step onboarding checklist.
- [agent/templates/service-spec.md](agent/templates/service-spec.md): Request template for describing new service integrations.
Workspace custom agent:
- [.github/agents/skeleton-services-mcp.agent.md](.github/agents/skeleton-services-mcp.agent.md)
- [.github/prompts/adapt-skeleton-service.prompt.md](.github/prompts/adapt-skeleton-service.prompt.md)
Use this custom agent when you want GitHub Copilot in VS Code to:
1. Configure new service adapters under `src/services`.
2. Register matching MCP tools in `src/mcp/server.js`.
3. Update env validation in `src/config/env.js`.
4. Preserve authorization and redaction behavior.
5. Add tests and documentation updates.
## Test Coverage
Integration tests in [tests/server.integration.test.js](tests/server.integration.test.js) cover:
- Healthcheck behavior
- Authorization on mutating tools
- Redaction behavior for secret output
HTTP transport tests in [tests/http.integration.test.js](tests/http.integration.test.js) cover:
- Unauthorized requests are rejected
- Authorized MCP initialization succeeds
- Internal failures return JSON-RPC-compatible error responses
- Health endpoint behavior
Vault token auth tests in [tests/vault-token-auth.test.js](tests/vault-token-auth.test.js) cover:
- Multi-user token index lookup by SHA-256 hash
- Inactive token rejection
- Scope/audience-aware authorization inputs
Production migration tests in [tests/vault-production.test.js](tests/vault-production.test.js) cover:
- Presence of Vault production scaffold files
- Raft config expectations
- Non-dev Vault compose command validation
- Conversion/bootstrap script help and bash syntax checks
## Extend The Skeleton
1. Add domain services under src/services.
2. Register new tools in src/mcp/server.js.
3. Add corresponding tests under tests.
4. Keep mutating tools behind authorization checks.
5. Keep secret-bearing fields redacted by default.
## Notes
- docker-compose.yml now runs Vault with Raft-backed storage for local persistence.
- The managed key script is an automation helper and not a Vault KMS/HSM auto-unseal backend.
- The migration scaffold starts with bootstrap-friendly defaults and still requires TLS, production auth methods, and credential rotation before real production use.
TDQS
B3.4/5.0
Scored across 11 tools
Disambiguation5/5
Each tool targets a distinct resource and action: config operations, secret operations, connection info, and healthcheck. There is no overlap or ambiguity.
Naming Consistency5/5
All tool names follow the consistent verb_noun pattern in snake_case (e.g., delete_config, list_secrets). Even connection_info and healthcheck fit the style.
Tool Count5/5
11 tools are well-scoped for a server managing configurations and secrets across two backends, with connection info and healthcheck. Neither too many nor too few.
Completeness5/5
Full CRUD for configs and secrets (get, set, delete, list), plus connection info and healthcheck. No missing operations for the domain.
Maintenance
ActivityMaintained
ResponsivenessSyncing