@kieksme/mcp-http-kit 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -2
  3. package/SECURITY.md +19 -0
  4. package/dist/auth/bearer.d.ts +14 -0
  5. package/dist/auth/bearer.d.ts.map +1 -0
  6. package/dist/auth/bearer.js +43 -0
  7. package/dist/auth/bearer.js.map +1 -0
  8. package/dist/auth/context.d.ts +7 -0
  9. package/dist/auth/context.d.ts.map +1 -0
  10. package/dist/auth/context.js +46 -0
  11. package/dist/auth/context.js.map +1 -0
  12. package/dist/auth/index.d.ts +25 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +34 -0
  15. package/dist/auth/index.js.map +1 -0
  16. package/dist/auth/oauth.d.ts +25 -0
  17. package/dist/auth/oauth.d.ts.map +1 -0
  18. package/dist/auth/oauth.js +161 -0
  19. package/dist/auth/oauth.js.map +1 -0
  20. package/dist/config.d.ts +30 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +201 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/http/app.d.ts +32 -0
  25. package/dist/http/app.d.ts.map +1 -0
  26. package/dist/http/app.js +127 -0
  27. package/dist/http/app.js.map +1 -0
  28. package/dist/http/host-guard.d.ts +22 -0
  29. package/dist/http/host-guard.d.ts.map +1 -0
  30. package/dist/http/host-guard.js +46 -0
  31. package/dist/http/host-guard.js.map +1 -0
  32. package/dist/http/index.d.ts +4 -0
  33. package/dist/http/index.d.ts.map +1 -0
  34. package/dist/http/index.js +4 -0
  35. package/dist/http/index.js.map +1 -0
  36. package/dist/http/node.d.ts +36 -0
  37. package/dist/http/node.d.ts.map +1 -0
  38. package/dist/http/node.js +74 -0
  39. package/dist/http/node.js.map +1 -0
  40. package/dist/index.d.ts +8 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +7 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/start.d.ts +16 -0
  45. package/dist/start.d.ts.map +1 -0
  46. package/dist/start.js +84 -0
  47. package/dist/start.js.map +1 -0
  48. package/dist/test/helpers.d.ts +35 -0
  49. package/dist/test/helpers.d.ts.map +1 -0
  50. package/dist/test/helpers.js +171 -0
  51. package/dist/test/helpers.js.map +1 -0
  52. package/dist/test/index.d.ts +2 -0
  53. package/dist/test/index.d.ts.map +1 -0
  54. package/dist/test/index.js +2 -0
  55. package/dist/test/index.js.map +1 -0
  56. package/dist/types.d.ts +123 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +2 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/upstream.d.ts +17 -0
  61. package/dist/upstream.d.ts.map +1 -0
  62. package/dist/upstream.js +49 -0
  63. package/dist/upstream.js.map +1 -0
  64. package/docs/migration.md +125 -0
  65. package/package.json +87 -4
  66. package/templates/Dockerfile +46 -0
  67. package/templates/railway.json +13 -0
  68. package/templates/server.json +79 -0
  69. package/templates/smithery.yaml +31 -0
@@ -0,0 +1,125 @@
1
+ # Migrating existing kieksme MCP servers to `@kieksme/mcp-http-kit`
2
+
3
+ This guide covers moving servers off hand-rolled HTTP/auth helpers onto the shared kit.
4
+
5
+ ## SDK decision
6
+
7
+ The kit targets **MCP TypeScript SDK v2**:
8
+
9
+ - `@modelcontextprotocol/server`
10
+ - `@modelcontextprotocol/node`
11
+
12
+ v2 is the current stable release line (spec 2026-07-28). The monolithic
13
+ `@modelcontextprotocol/sdk` (v1) is legacy. New and migrated kieksme servers
14
+ should use v2; do not keep a v1 adapter in the kit.
15
+
16
+ | Before | After |
17
+ | --- | --- |
18
+ | `mcp-enhancv`, `mcp-balena` | Already on v2 — swap local `http.ts` for the kit |
19
+ | `mcp-contabo`, `mcp-listmonk`, `mcp-swetrix`, `mcp-hashnode` | On v1 `@modelcontextprotocol/sdk` — upgrade packages + imports, then adopt the kit |
20
+
21
+ ## Env var rename map
22
+
23
+ | Legacy | Canonical |
24
+ | --- | --- |
25
+ | `MCP_HTTP_AUTH_TOKEN` / `MCP_SERVER_AUTH_TOKEN` | `MCP_AUTH_TOKEN` (comma list OK) |
26
+ | `MCP_HTTP_PORT` (as primary) | `PORT` (fallback still reads `MCP_HTTP_PORT`) |
27
+ | `MCP_HTTP_ALLOWED_HOSTS` | `MCP_ALLOWED_HOSTS` |
28
+ | `MCP_HTTP_ALLOWED_ORIGINS` | `MCP_ALLOWED_ORIGINS` |
29
+ | `MCP_HTTP_ENDPOINT` | `MCP_HTTP_PATH` (default `/mcp`) |
30
+ | `HEALTH_ENDPOINT` | `MCP_HEALTH_PATH` (default `/health`) |
31
+
32
+ Legacy names still work and emit a one-line warning.
33
+
34
+ New knobs:
35
+
36
+ - `MCP_AUTH_MODE=bearer|oauth|none`
37
+ - `MCP_AUTH_ALLOW_NONE=true` (only for public read-only demos off localhost)
38
+ - OAuth: `MCP_OAUTH_RESOURCE`, `MCP_OAUTH_AUTHORIZATION_SERVERS`, `MCP_OAUTH_AUDIENCES`, `MCP_OAUTH_JWKS_URL`, `MCP_OAUTH_REQUIRED_SCOPES`, `MCP_OAUTH_SCOPES_SUPPORTED`
39
+
40
+ ## Code shape
41
+
42
+ **Before (enhancv-style):** local `parseHttpConfig` + `createHttpServer` + Host check before `/health`.
43
+
44
+ **After:**
45
+
46
+ ```ts
47
+ import { McpServer } from '@modelcontextprotocol/server';
48
+ import { loadConfig, startMcpServer } from '@kieksme/mcp-http-kit';
49
+
50
+ function createServer(ctx) {
51
+ const server = new McpServer({ name: 'my-mcp', version: '1.0.0' });
52
+ // register tools; ctx.auth.scopes available under oauth
53
+ return server;
54
+ }
55
+
56
+ await startMcpServer({ createServer, config: loadConfig(process.env, { realm: 'my-mcp' }) });
57
+ ```
58
+
59
+ Important behavior change: **`/health` is served before Host/Origin checks** so Railway (and similar) healthchecks keep working when `MCP_ALLOWED_HOSTS` is tight.
60
+
61
+ If you previously exposed `/healthz` (listmonk) or custom health JSON, use:
62
+
63
+ ```ts
64
+ const config = loadConfig(process.env, {
65
+ realm: 'my-mcp',
66
+ healthPathAliases: ['/healthz'], // or MCP_HEALTH_ALIASES=/healthz
67
+ });
68
+
69
+ await startMcpServer({
70
+ createServer,
71
+ config,
72
+ getHealthInfo: () => ({ version: VERSION, tools: TOOL_COUNT }),
73
+ });
74
+ ```
75
+
76
+ ## Per-request upstream credentials
77
+
78
+ ```ts
79
+ import { resolveUpstreamCredential } from '@kieksme/mcp-http-kit';
80
+
81
+ const cred = resolveUpstreamCredential(ctx.headers, {
82
+ mode: process.env.UPSTREAM_CREDENTIAL_MODE === 'header' ? 'header' : 'env',
83
+ envVar: 'CONTABO_API_KEY',
84
+ headerName: 'X-Contabo-Api-Key',
85
+ authMode: ctx.auth.mode,
86
+ });
87
+ ```
88
+
89
+ Header mode is rejected when `MCP_AUTH_MODE=none`.
90
+
91
+ ## Deploy templates
92
+
93
+ Copy from this repo’s `templates/`:
94
+
95
+ - `Dockerfile` — multi-stage, non-root, `HEALTHCHECK` on `/health`
96
+ - `railway.json` — `healthcheckPath: /health`
97
+ - `server.json` — npm + OCI + `remotes` streamable-http for the official registry
98
+ - `smithery.yaml` — container runtime, HTTP start command
99
+
100
+ ## Contract tests
101
+
102
+ ```ts
103
+ import { createMcpHttpApp } from '@kieksme/mcp-http-kit';
104
+ import { createAppTestClient, createKitContractAssertions } from '@kieksme/mcp-http-kit/test';
105
+
106
+ const config = loadConfig({ /* test env */ });
107
+ const app = createMcpHttpApp({ createServer, config });
108
+ const client = createAppTestClient(app, config);
109
+ const assertions = createKitContractAssertions(client, config);
110
+
111
+ await assertions.healthUnauthenticated();
112
+ await assertions.unauthorizedWithoutToken();
113
+ await assertions.forbiddenOnBadHost();
114
+ await assertions.initializeAndListTools(token);
115
+ ```
116
+
117
+ ## Checklist
118
+
119
+ 1. Bump to `@modelcontextprotocol/server` + `@modelcontextprotocol/node` (drop v1 sdk).
120
+ 2. Add `@kieksme/mcp-http-kit` dependency.
121
+ 3. Replace local HTTP/auth modules with `loadConfig` + `startMcpServer`.
122
+ 4. Rename Railway/Docker env vars (or keep aliases temporarily).
123
+ 5. Ensure `/health` is what Railway probes (kit default).
124
+ 6. Copy deploy templates; publish `server.json` via `mcp-publisher` if applicable.
125
+ 7. Import kit contract assertions in CI.
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@kieksme/mcp-http-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Shared Streamable HTTP transport, auth (bearer + OAuth 2.1 resource server), and deploy tooling for kieksme MCP servers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./test": {
14
+ "types": "./dist/test/index.d.ts",
15
+ "import": "./dist/test/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "templates",
22
+ "docs",
23
+ "README.md",
24
+ "LICENSE",
25
+ "SECURITY.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.14.0"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/kieksme/mcp-http-kit.git"
33
+ },
34
+ "homepage": "https://github.com/kieksme/mcp-http-kit#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/kieksme/mcp-http-kit/issues"
37
+ },
38
+ "license": "MIT",
39
+ "keywords": [
40
+ "mcp",
41
+ "model-context-protocol",
42
+ "streamable-http",
43
+ "oauth",
44
+ "railway",
45
+ "kieksme"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
51
+ "scripts": {
52
+ "prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
53
+ "build": "tsc -p tsconfig.build.json",
54
+ "prepare": "node scripts/prepare.mjs",
55
+ "typecheck": "tsc -p tsconfig.json",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "lint": "eslint .",
59
+ "lint:fix": "eslint . --fix",
60
+ "pack:check": "node scripts/pack-check.mjs",
61
+ "example:echo": "tsx examples/echo-server/src/index.ts"
62
+ },
63
+ "dependencies": {
64
+ "@modelcontextprotocol/node": "^2.1.0",
65
+ "@modelcontextprotocol/server": "^2.3.0",
66
+ "jose": "^6.2.13"
67
+ },
68
+ "peerDependencies": {
69
+ "@modelcontextprotocol/node": "^2.1.0",
70
+ "@modelcontextprotocol/server": "^2.3.0"
71
+ },
72
+ "devDependencies": {
73
+ "@eslint/js": "^9.39.5",
74
+ "@modelcontextprotocol/client": "^2.3.1",
75
+ "@types/node": "^22.0.0",
76
+ "eslint": "^9.39.2",
77
+ "tsx": "^4.20.0",
78
+ "typescript": "^5.9.3",
79
+ "typescript-eslint": "^8.71.1",
80
+ "vitest": "^3.2.4",
81
+ "zod": "^4.6.7"
82
+ },
83
+ "pnpm": {
84
+ "onlyBuiltDependencies": [
85
+ "esbuild"
86
+ ]
87
+ },
88
+ "packageManager": "pnpm@10.33.2"
89
+ }
@@ -0,0 +1,46 @@
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # Template Dockerfile for a kieksme MCP server using @kieksme/mcp-http-kit.
4
+ # Copy next to your server package.json and adjust COPY paths / labels.
5
+ #
6
+ # Runtime secrets (never bake into the image):
7
+ # MCP_AUTH_TOKEN — bearer token(s), comma-separated for rotation
8
+ # <SERVICE>_API_KEY — upstream API credentials
9
+ #
10
+ # Railway / containers: PORT is honored by the kit (fallback MCP_HTTP_PORT).
11
+
12
+ ARG NODE_VERSION=22-alpine
13
+ ARG PNPM_VERSION=10.33.2
14
+
15
+ # ---- build ----
16
+ FROM node:${NODE_VERSION} AS build
17
+ ARG PNPM_VERSION
18
+ WORKDIR /app
19
+ RUN npm install -g pnpm@${PNPM_VERSION}
20
+ COPY package.json pnpm-lock.yaml ./
21
+ RUN pnpm install --frozen-lockfile
22
+ COPY tsconfig.json tsconfig.build.json ./
23
+ COPY src ./src
24
+ RUN pnpm build && pnpm prune --prod
25
+
26
+ # ---- runtime ----
27
+ FROM node:${NODE_VERSION} AS runtime
28
+ LABEL org.opencontainers.image.title="REPLACE_WITH_SERVER_NAME" \
29
+ org.opencontainers.image.description="MCP server (Streamable HTTP)" \
30
+ org.opencontainers.image.source="https://github.com/kieksme/REPLACE_WITH_REPO" \
31
+ org.opencontainers.image.licenses="MIT"
32
+ ENV NODE_ENV=production \
33
+ MCP_TRANSPORT=http \
34
+ MCP_HTTP_HOST=0.0.0.0 \
35
+ PORT=3000 \
36
+ MCP_ALLOWED_HOSTS=localhost,127.0.0.1
37
+ WORKDIR /app
38
+ COPY --from=build --chown=node:node /app/package.json ./package.json
39
+ COPY --chown=node:node LICENSE ./LICENSE
40
+ COPY --from=build --chown=node:node /app/dist ./dist
41
+ COPY --from=build --chown=node:node /app/node_modules ./node_modules
42
+ USER node
43
+ EXPOSE 3000
44
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
45
+ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||process.env.MCP_HTTP_PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
46
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://railway.com/railway.schema.json",
3
+ "build": {
4
+ "builder": "DOCKERFILE",
5
+ "dockerfilePath": "Dockerfile"
6
+ },
7
+ "deploy": {
8
+ "healthcheckPath": "/health",
9
+ "healthcheckTimeout": 30,
10
+ "restartPolicyType": "ON_FAILURE",
11
+ "restartPolicyMaxRetries": 3
12
+ }
13
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.kieksme/REPLACE_WITH_SERVER_NAME",
4
+ "title": "REPLACE_WITH_TITLE",
5
+ "description": "REPLACE_WITH_DESCRIPTION",
6
+ "repository": {
7
+ "url": "https://github.com/kieksme/REPLACE_WITH_REPO",
8
+ "source": "github"
9
+ },
10
+ "websiteUrl": "https://github.com/kieksme/REPLACE_WITH_REPO#readme",
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "@kieksme/REPLACE_WITH_NPM_NAME",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "npx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "REPLACE_WITH_UPSTREAM_API_KEY_ENV",
25
+ "description": "Upstream API key for the service this MCP server wraps.",
26
+ "isRequired": true,
27
+ "isSecret": true,
28
+ "format": "string"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "registryType": "oci",
34
+ "identifier": "ghcr.io/kieksme/REPLACE_WITH_REPO",
35
+ "version": "0.1.0",
36
+ "transport": {
37
+ "type": "streamable-http",
38
+ "url": "http://localhost:3000/mcp"
39
+ },
40
+ "environmentVariables": [
41
+ {
42
+ "name": "MCP_AUTH_TOKEN",
43
+ "description": "Bearer token MCP clients must send (Authorization: Bearer …). Comma-separated list allowed for rotation.",
44
+ "isRequired": true,
45
+ "isSecret": true,
46
+ "format": "string"
47
+ },
48
+ {
49
+ "name": "REPLACE_WITH_UPSTREAM_API_KEY_ENV",
50
+ "description": "Upstream API key for the service this MCP server wraps.",
51
+ "isRequired": true,
52
+ "isSecret": true,
53
+ "format": "string"
54
+ },
55
+ {
56
+ "name": "MCP_ALLOWED_HOSTS",
57
+ "description": "Comma-separated Host allowlist (required when binding beyond localhost).",
58
+ "isRequired": true,
59
+ "isSecret": false,
60
+ "format": "string"
61
+ }
62
+ ]
63
+ }
64
+ ],
65
+ "remotes": [
66
+ {
67
+ "type": "streamable-http",
68
+ "url": "https://REPLACE_WITH_PUBLIC_HOST/mcp",
69
+ "headers": [
70
+ {
71
+ "name": "Authorization",
72
+ "description": "Bearer token (MCP_AUTH_TOKEN). Format: Bearer <token>",
73
+ "isRequired": true,
74
+ "isSecret": true
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
@@ -0,0 +1,31 @@
1
+ # Smithery project config — https://smithery.ai/docs/build/project-config
2
+ #
3
+ # Container runtime: Smithery builds the Dockerfile and connects over
4
+ # Streamable HTTP. The kit honors PORT (Smithery sets it) and serves MCP at
5
+ # /mcp by default. Copy to the server repo root and fill configSchema.
6
+
7
+ runtime: container
8
+
9
+ build:
10
+ dockerfile: Dockerfile
11
+ dockerBuildPath: .
12
+
13
+ startCommand:
14
+ type: http
15
+ configSchema:
16
+ type: object
17
+ properties:
18
+ mcpAuthToken:
19
+ type: string
20
+ title: MCP bearer token
21
+ description: Value for MCP_AUTH_TOKEN (clients send Authorization: Bearer …).
22
+ upstreamApiKey:
23
+ type: string
24
+ title: Upstream API key
25
+ description: Service API key mapped to your server's env var.
26
+ required:
27
+ - mcpAuthToken
28
+ - upstreamApiKey
29
+ exampleConfig:
30
+ mcpAuthToken: "replace-me"
31
+ upstreamApiKey: "replace-me"