Skip to main content
Glama
LesterAJohn

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