@kieksme/mcp-http-kit 0.0.0-stage → 0.1.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 +177 -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 +23 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +178 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/http/app.d.ts +27 -0
  25. package/dist/http/app.d.ts.map +1 -0
  26. package/dist/http/app.js +125 -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 +83 -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 +169 -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 +112 -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 +110 -0
  65. package/package.json +94 -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
package/dist/start.js ADDED
@@ -0,0 +1,83 @@
1
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
2
+ import { loadConfig } from './config.js';
3
+ import { createMcpHttpApp } from './http/app.js';
4
+ import { createNodeHttpServer } from './http/node.js';
5
+ function publicUrl(config) {
6
+ const host = config.http.host === '0.0.0.0' ? '127.0.0.1' : config.http.host;
7
+ return `http://${host}:${config.http.port}${config.http.path}`;
8
+ }
9
+ /**
10
+ * Start an MCP server over stdio or stateless Streamable HTTP based on config.
11
+ *
12
+ * ```ts
13
+ * await startMcpServer({
14
+ * createServer: (ctx) => {
15
+ * const server = new McpServer({ name: 'demo', version: '1.0.0' });
16
+ * // register tools; ctx.auth.scopes available for oauth
17
+ * return server;
18
+ * },
19
+ * });
20
+ * ```
21
+ */
22
+ export async function startMcpServer(options) {
23
+ const config = options.config ?? loadConfig(options.env ?? process.env);
24
+ for (const warning of config.warnings) {
25
+ console.warn(`[mcp-http-kit] ${warning}`);
26
+ }
27
+ if (config.transport === 'stdio') {
28
+ const handle = serveStdio(() => options.createServer({
29
+ auth: { mode: 'none', scopes: [] },
30
+ headers: new Headers(),
31
+ url: new URL('stdio://local'),
32
+ era: 'modern',
33
+ }));
34
+ const close = async () => {
35
+ await handle.close();
36
+ };
37
+ if (options.signal) {
38
+ if (options.signal.aborted)
39
+ await close();
40
+ else
41
+ options.signal.addEventListener('abort', () => void close(), { once: true });
42
+ }
43
+ return { close, config };
44
+ }
45
+ const app = createMcpHttpApp({
46
+ createServer: options.createServer,
47
+ config,
48
+ });
49
+ const server = createNodeHttpServer({ app, config });
50
+ await new Promise((resolve, reject) => {
51
+ server.once('error', reject);
52
+ server.listen(config.http.port, config.http.host, () => resolve());
53
+ });
54
+ const url = publicUrl(config);
55
+ options.onListening?.({ host: config.http.host, port: config.http.port, url });
56
+ let closing;
57
+ const close = async () => {
58
+ if (closing)
59
+ return closing;
60
+ closing = (async () => {
61
+ await new Promise((resolve, reject) => {
62
+ server.close(error => (error ? reject(error) : resolve()));
63
+ });
64
+ await app.close();
65
+ })();
66
+ return closing;
67
+ };
68
+ const onSignal = () => {
69
+ void close().finally(() => {
70
+ // allow process to exit after graceful shutdown
71
+ });
72
+ };
73
+ process.once('SIGTERM', onSignal);
74
+ process.once('SIGINT', onSignal);
75
+ if (options.signal) {
76
+ if (options.signal.aborted)
77
+ await close();
78
+ else
79
+ options.signal.addEventListener('abort', () => void close(), { once: true });
80
+ }
81
+ return { close, url, config };
82
+ }
83
+ //# sourceMappingURL=start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { McpHttpApp } from '../http/app.js';
2
+ import type { KitConfig } from '../types.js';
3
+ export type KitHttpTestClient = {
4
+ baseUrl: string;
5
+ health: () => Promise<Response>;
6
+ mcp: (init?: RequestInit & {
7
+ token?: string;
8
+ host?: string;
9
+ origin?: string;
10
+ }) => Promise<Response>;
11
+ fetch: (path: string, init?: RequestInit) => Promise<Response>;
12
+ };
13
+ /** Parse a Streamable HTTP MCP response (JSON body or SSE `data:` frames). */
14
+ export declare function readMcpJsonResponse(response: Response): Promise<unknown>;
15
+ /** In-process client against a kit `McpHttpApp.fetch` (no real listen required). */
16
+ export declare function createAppTestClient(app: McpHttpApp, config: KitConfig): KitHttpTestClient;
17
+ export type KitContractAssertions = {
18
+ /** GET /health without auth → 200 */
19
+ healthUnauthenticated: () => Promise<void>;
20
+ /** POST /mcp without token → 401 (+ WWW-Authenticate) */
21
+ unauthorizedWithoutToken: () => Promise<void>;
22
+ /** Bad Host → 403 (health stays 200 even with bad Host) */
23
+ forbiddenOnBadHost: () => Promise<void>;
24
+ /** Bad Origin → 403 on MCP path */
25
+ forbiddenOnBadOrigin: () => Promise<void>;
26
+ /** initialize + tools/list round-trip with a valid token */
27
+ initializeAndListTools: (token: string) => Promise<{
28
+ tools: unknown[];
29
+ }>;
30
+ };
31
+ /**
32
+ * Reusable HTTP contract checks servers can call from their own vitest suites.
33
+ */
34
+ export declare function createKitContractAssertions(client: KitHttpTestClient, config: KitConfig): KitContractAssertions;
35
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/test/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpG,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChE,CAAC;AAEF,8EAA8E;AAC9E,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB9E;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,GAAG,iBAAiB,CA6BzF;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,qCAAqC;IACrC,qBAAqB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,yDAAyD;IACzD,wBAAwB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,2DAA2D;IAC3D,kBAAkB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,mCAAmC;IACnC,oBAAoB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,4DAA4D;IAC5D,sBAAsB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;CAC1E,CAAC;AAEF;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,SAAS,GAChB,qBAAqB,CAkHvB"}
@@ -0,0 +1,169 @@
1
+ /** Parse a Streamable HTTP MCP response (JSON body or SSE `data:` frames). */
2
+ export async function readMcpJsonResponse(response) {
3
+ const contentType = response.headers.get('content-type') ?? '';
4
+ const text = await response.text();
5
+ if (contentType.includes('application/json') || text.trimStart().startsWith('{')) {
6
+ return JSON.parse(text);
7
+ }
8
+ // SSE: take the last JSON payload from data: lines
9
+ const payloads = [];
10
+ for (const line of text.split(/\r?\n/)) {
11
+ if (!line.startsWith('data:'))
12
+ continue;
13
+ const data = line.slice(5).trim();
14
+ if (!data || data === '[DONE]')
15
+ continue;
16
+ try {
17
+ payloads.push(JSON.parse(data));
18
+ }
19
+ catch {
20
+ // ignore non-JSON data frames
21
+ }
22
+ }
23
+ if (payloads.length === 0) {
24
+ throw new Error(`No JSON payload in MCP response (${contentType}): ${text.slice(0, 200)}`);
25
+ }
26
+ return payloads[payloads.length - 1];
27
+ }
28
+ /** In-process client against a kit `McpHttpApp.fetch` (no real listen required). */
29
+ export function createAppTestClient(app, config) {
30
+ const originHost = config.http.allowedHosts[0] ?? '127.0.0.1';
31
+ const baseUrl = `http://${originHost}:${config.http.port}`;
32
+ const fetch = (path, init) => {
33
+ const headers = new Headers(init?.headers);
34
+ if (!headers.has('host'))
35
+ headers.set('host', originHost);
36
+ return app.fetch(new Request(`${baseUrl}${path}`, { ...init, headers }));
37
+ };
38
+ return {
39
+ baseUrl,
40
+ health: () => fetch(config.http.healthPath, { method: 'GET' }),
41
+ mcp: (init = {}) => {
42
+ const headers = new Headers(init.headers);
43
+ if (init.token)
44
+ headers.set('authorization', `Bearer ${init.token}`);
45
+ if (init.host)
46
+ headers.set('host', init.host);
47
+ if (init.origin)
48
+ headers.set('origin', init.origin);
49
+ if (!headers.has('accept')) {
50
+ headers.set('accept', 'application/json, text/event-stream');
51
+ }
52
+ if (!headers.has('content-type') && init.body) {
53
+ headers.set('content-type', 'application/json');
54
+ }
55
+ const { token: _t, host: _h, origin: _o, ...rest } = init;
56
+ return fetch(config.http.path, { method: 'POST', ...rest, headers });
57
+ },
58
+ fetch,
59
+ };
60
+ }
61
+ /**
62
+ * Reusable HTTP contract checks servers can call from their own vitest suites.
63
+ */
64
+ export function createKitContractAssertions(client, config) {
65
+ return {
66
+ async healthUnauthenticated() {
67
+ const response = await client.health();
68
+ if (response.status !== 200) {
69
+ throw new Error(`Expected /health 200, got ${response.status}`);
70
+ }
71
+ const body = (await response.json());
72
+ if (body.status !== 'ok') {
73
+ throw new Error(`Expected health status ok, got ${JSON.stringify(body)}`);
74
+ }
75
+ // Health must ignore Host allowlist (Railway probes).
76
+ const evil = await client.fetch(config.http.healthPath, {
77
+ method: 'GET',
78
+ headers: { host: 'evil.example' },
79
+ });
80
+ if (evil.status !== 200) {
81
+ throw new Error(`Expected /health 200 with bad Host, got ${evil.status}`);
82
+ }
83
+ },
84
+ async unauthorizedWithoutToken() {
85
+ if (config.auth.mode === 'none')
86
+ return;
87
+ const response = await client.mcp({
88
+ body: JSON.stringify({
89
+ jsonrpc: '2.0',
90
+ id: 1,
91
+ method: 'initialize',
92
+ params: {
93
+ protocolVersion: '2025-06-18',
94
+ capabilities: {},
95
+ clientInfo: { name: 'kit-test', version: '0' },
96
+ },
97
+ }),
98
+ });
99
+ if (response.status !== 401) {
100
+ throw new Error(`Expected 401 without token, got ${response.status}`);
101
+ }
102
+ if (!response.headers.get('www-authenticate')) {
103
+ throw new Error('Expected WWW-Authenticate header on 401');
104
+ }
105
+ },
106
+ async forbiddenOnBadHost() {
107
+ const token = config.auth.mode === 'bearer' ? config.auth.tokens[0] : 'dummy';
108
+ const response = await client.mcp({
109
+ host: 'evil.example',
110
+ ...(token ? { token } : {}),
111
+ body: '{}',
112
+ });
113
+ if (response.status !== 403) {
114
+ throw new Error(`Expected 403 for bad Host, got ${response.status}`);
115
+ }
116
+ },
117
+ async forbiddenOnBadOrigin() {
118
+ const token = config.auth.mode === 'bearer' ? config.auth.tokens[0] : 'dummy';
119
+ const response = await client.mcp({
120
+ ...(token ? { token } : {}),
121
+ origin: 'https://evil.example',
122
+ body: '{}',
123
+ });
124
+ if (response.status !== 403) {
125
+ throw new Error(`Expected 403 for bad Origin, got ${response.status}`);
126
+ }
127
+ },
128
+ async initializeAndListTools(token) {
129
+ const initBody = {
130
+ jsonrpc: '2.0',
131
+ id: 1,
132
+ method: 'initialize',
133
+ params: {
134
+ protocolVersion: '2025-06-18',
135
+ capabilities: {},
136
+ clientInfo: { name: 'kit-test', version: '0' },
137
+ },
138
+ };
139
+ const initResponse = await client.mcp({
140
+ token,
141
+ body: JSON.stringify(initBody),
142
+ });
143
+ if (!initResponse.ok) {
144
+ throw new Error(`initialize failed: ${initResponse.status} ${await initResponse.text()}`);
145
+ }
146
+ await readMcpJsonResponse(initResponse);
147
+ const listBody = {
148
+ jsonrpc: '2.0',
149
+ id: 2,
150
+ method: 'tools/list',
151
+ params: {},
152
+ };
153
+ const listResponse = await client.mcp({
154
+ token,
155
+ headers: {
156
+ 'mcp-protocol-version': '2025-06-18',
157
+ },
158
+ body: JSON.stringify(listBody),
159
+ });
160
+ if (!listResponse.ok) {
161
+ throw new Error(`tools/list failed: ${listResponse.status} ${await listResponse.text()}`);
162
+ }
163
+ const payload = (await readMcpJsonResponse(listResponse));
164
+ const tools = payload.result?.tools ?? payload.tools ?? [];
165
+ return { tools };
166
+ },
167
+ };
168
+ }
169
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/test/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { createAppTestClient, createKitContractAssertions, readMcpJsonResponse, type KitContractAssertions, type KitHttpTestClient, } from './helpers.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createAppTestClient, createKitContractAssertions, readMcpJsonResponse, } from './helpers.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,mBAAmB,GAGpB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,112 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { AuthInfo, McpServer, Server } from '@modelcontextprotocol/server';
3
+ /** Transport selected by `MCP_TRANSPORT`. */
4
+ export type TransportMode = 'stdio' | 'http';
5
+ /** Authentication strategy for Streamable HTTP. */
6
+ export type AuthMode = 'bearer' | 'oauth' | 'none';
7
+ /** How upstream service credentials are resolved per request. */
8
+ export type UpstreamCredentialMode = 'env' | 'header';
9
+ export type BearerAuthConfig = {
10
+ mode: 'bearer';
11
+ /** Accepted bearer tokens (rotation-friendly). */
12
+ tokens: string[];
13
+ };
14
+ export type OAuthAuthConfig = {
15
+ mode: 'oauth';
16
+ /** Canonical resource identifier (usually the public MCP URL). */
17
+ resource: string;
18
+ /** Authorization server issuer URLs (RFC 9728 `authorization_servers`). */
19
+ authorizationServers: string[];
20
+ /** Expected JWT audience values (resource indicator and/or explicit aud). */
21
+ audiences: string[];
22
+ /** Optional JWKS URL override; otherwise derived from AS metadata / well-known. */
23
+ jwksUrl?: string;
24
+ /** Required scopes (all must be present). Empty = any scope / no scope check. */
25
+ requiredScopes: string[];
26
+ /** Scopes advertised in protected-resource metadata. */
27
+ scopesSupported: string[];
28
+ };
29
+ export type NoneAuthConfig = {
30
+ mode: 'none';
31
+ /** Must be true when binding beyond localhost. */
32
+ allowPublic: boolean;
33
+ };
34
+ export type AuthConfig = BearerAuthConfig | OAuthAuthConfig | NoneAuthConfig;
35
+ export type HttpListenConfig = {
36
+ host: string;
37
+ port: number;
38
+ /** MCP endpoint path (default `/mcp`). */
39
+ path: string;
40
+ /** Unauthenticated health path (default `/health`). */
41
+ healthPath: string;
42
+ allowedHosts: string[];
43
+ allowedOrigins: string[];
44
+ /** Max request body size in bytes. */
45
+ maxBodyBytes: number;
46
+ /** When true, GET/DELETE on the MCP path are forwarded (default: 405). */
47
+ enableSessionMethods: boolean;
48
+ };
49
+ export type KitConfig = {
50
+ transport: TransportMode;
51
+ http: HttpListenConfig;
52
+ auth: AuthConfig;
53
+ /** Realm used in WWW-Authenticate for bearer. */
54
+ realm: string;
55
+ /** Warnings emitted while loading (e.g. deprecated env aliases). */
56
+ warnings: string[];
57
+ };
58
+ /** Auth context attached to an accepted HTTP request. */
59
+ export type AuthContext = {
60
+ mode: AuthMode;
61
+ /** Bearer token or access token string when present. */
62
+ token?: string;
63
+ /** JWT `sub` (oauth) when available. */
64
+ subject?: string;
65
+ /** Granted scopes (oauth) or empty for bearer/none. */
66
+ scopes: string[];
67
+ /** Raw JWT claims (oauth only). */
68
+ claims?: Record<string, unknown>;
69
+ };
70
+ export type RequestContext = {
71
+ auth: AuthContext;
72
+ headers: Headers;
73
+ /** Absolute request URL. */
74
+ url: URL;
75
+ /** Protocol era from the SDK factory (`modern` | `legacy`). */
76
+ era: 'legacy' | 'modern';
77
+ /** SDK pass-through auth info (scopes available to `requireScopes` / tool handlers). */
78
+ authInfo?: AuthInfo;
79
+ };
80
+ export type ServerFactory = (context: RequestContext) => McpServer | Server | Promise<McpServer | Server>;
81
+ export type UpstreamCredentialOptions = {
82
+ mode: UpstreamCredentialMode;
83
+ /** Env var used when `mode === 'env'`. */
84
+ envVar: string;
85
+ /** Header name used when `mode === 'header'` (e.g. `X-Contabo-Api-Key`). */
86
+ headerName: string;
87
+ /** Current kit auth mode — header mode is refused when auth is `none`. */
88
+ authMode: AuthMode;
89
+ env?: NodeJS.ProcessEnv;
90
+ };
91
+ export type NodeHttpRequest = IncomingMessage;
92
+ export type NodeHttpResponse = ServerResponse;
93
+ export type StartMcpServerOptions = {
94
+ createServer: ServerFactory;
95
+ /** Pre-loaded config; defaults to `loadConfig()`. */
96
+ config?: KitConfig;
97
+ env?: NodeJS.ProcessEnv;
98
+ /** Optional hook fired after the HTTP server starts listening. */
99
+ onListening?: (info: {
100
+ host: string;
101
+ port: number;
102
+ url: string;
103
+ }) => void;
104
+ signal?: AbortSignal;
105
+ };
106
+ export type StartedServer = {
107
+ close: () => Promise<void>;
108
+ /** Present in HTTP mode. */
109
+ url?: string;
110
+ config: KitConfig;
111
+ };
112
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import type { AuthMode, UpstreamCredentialMode, UpstreamCredentialOptions } from './types.js';
2
+ export type UpstreamCredentialResult = {
3
+ mode: UpstreamCredentialMode;
4
+ value: string;
5
+ source: 'env' | 'header';
6
+ };
7
+ /**
8
+ * Resolve per-request upstream credentials (e.g. `X-Contabo-Api-Key`).
9
+ *
10
+ * Header mode is refused when kit auth is `none` — otherwise anyone could
11
+ * smuggle upstream keys through an open endpoint.
12
+ */
13
+ export declare function resolveUpstreamCredential(headers: Headers | Record<string, string | string[] | undefined>, options: UpstreamCredentialOptions): UpstreamCredentialResult;
14
+ export declare function parseUpstreamCredentialMode(raw: string | undefined, fallback?: UpstreamCredentialMode): UpstreamCredentialMode;
15
+ /** Guard used by servers that expose a documented `X-<Service>-*` header. */
16
+ export declare function assertHeaderModeAllowed(authMode: AuthMode): void;
17
+ //# sourceMappingURL=upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.d.ts","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE9F,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,GAAG,QAAQ,CAAC;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAChE,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CAsB1B;AAED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,GAAE,sBAA8B,GACvC,sBAAsB,CAKxB;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAIhE"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Resolve per-request upstream credentials (e.g. `X-Contabo-Api-Key`).
3
+ *
4
+ * Header mode is refused when kit auth is `none` — otherwise anyone could
5
+ * smuggle upstream keys through an open endpoint.
6
+ */
7
+ export function resolveUpstreamCredential(headers, options) {
8
+ const { mode, envVar, headerName, authMode, env = process.env } = options;
9
+ if (mode === 'header' && authMode === 'none') {
10
+ throw new Error('Upstream credential mode "header" is not allowed when MCP_AUTH_MODE=none');
11
+ }
12
+ if (mode === 'env') {
13
+ const value = env[envVar]?.trim();
14
+ if (!value) {
15
+ throw new Error(`${envVar} is required when upstream credential mode is "env"`);
16
+ }
17
+ return { mode: 'env', value, source: 'env' };
18
+ }
19
+ const raw = getHeader(headers, headerName);
20
+ if (!raw?.trim()) {
21
+ throw new Error(`Missing required upstream credential header: ${headerName}`);
22
+ }
23
+ return { mode: 'header', value: raw.trim(), source: 'header' };
24
+ }
25
+ export function parseUpstreamCredentialMode(raw, fallback = 'env') {
26
+ if (!raw)
27
+ return fallback;
28
+ const value = raw.trim().toLowerCase();
29
+ if (value === 'env' || value === 'header')
30
+ return value;
31
+ throw new Error(`Upstream credential mode must be env|header (got ${JSON.stringify(raw)})`);
32
+ }
33
+ /** Guard used by servers that expose a documented `X-<Service>-*` header. */
34
+ export function assertHeaderModeAllowed(authMode) {
35
+ if (authMode === 'none') {
36
+ throw new Error('Header-based upstream credentials require bearer or oauth auth');
37
+ }
38
+ }
39
+ function getHeader(headers, name) {
40
+ if (typeof headers.get === 'function') {
41
+ return headers.get(name) ?? undefined;
42
+ }
43
+ const record = headers;
44
+ const direct = record[name] ?? record[name.toLowerCase()];
45
+ if (Array.isArray(direct))
46
+ return direct[0];
47
+ return direct;
48
+ }
49
+ //# sourceMappingURL=upstream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.js","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAgE,EAChE,OAAkC;IAElC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IAE1E,IAAI,IAAI,KAAK,QAAQ,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,0EAA0E,CAC3E,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,qDAAqD,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC3C,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gDAAgD,UAAU,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,GAAuB,EACvB,WAAmC,KAAK;IAExC,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxD,MAAM,IAAI,KAAK,CAAC,oDAAoD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC9F,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,uBAAuB,CAAC,QAAkB;IACxD,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAChB,OAAgE,EAChE,IAAY;IAEZ,IAAI,OAAQ,OAAmB,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QACnD,OAAQ,OAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;IACrD,CAAC;IACD,MAAM,MAAM,GAAG,OAAwD,CAAC;IACxE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,110 @@
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
+ ## Per-request upstream credentials
62
+
63
+ ```ts
64
+ import { resolveUpstreamCredential } from '@kieksme/mcp-http-kit';
65
+
66
+ const cred = resolveUpstreamCredential(ctx.headers, {
67
+ mode: process.env.UPSTREAM_CREDENTIAL_MODE === 'header' ? 'header' : 'env',
68
+ envVar: 'CONTABO_API_KEY',
69
+ headerName: 'X-Contabo-Api-Key',
70
+ authMode: ctx.auth.mode,
71
+ });
72
+ ```
73
+
74
+ Header mode is rejected when `MCP_AUTH_MODE=none`.
75
+
76
+ ## Deploy templates
77
+
78
+ Copy from this repo’s `templates/`:
79
+
80
+ - `Dockerfile` — multi-stage, non-root, `HEALTHCHECK` on `/health`
81
+ - `railway.json` — `healthcheckPath: /health`
82
+ - `server.json` — npm + OCI + `remotes` streamable-http for the official registry
83
+ - `smithery.yaml` — container runtime, HTTP start command
84
+
85
+ ## Contract tests
86
+
87
+ ```ts
88
+ import { createMcpHttpApp } from '@kieksme/mcp-http-kit';
89
+ import { createAppTestClient, createKitContractAssertions } from '@kieksme/mcp-http-kit/test';
90
+
91
+ const config = loadConfig({ /* test env */ });
92
+ const app = createMcpHttpApp({ createServer, config });
93
+ const client = createAppTestClient(app, config);
94
+ const assertions = createKitContractAssertions(client, config);
95
+
96
+ await assertions.healthUnauthenticated();
97
+ await assertions.unauthorizedWithoutToken();
98
+ await assertions.forbiddenOnBadHost();
99
+ await assertions.initializeAndListTools(token);
100
+ ```
101
+
102
+ ## Checklist
103
+
104
+ 1. Bump to `@modelcontextprotocol/server` + `@modelcontextprotocol/node` (drop v1 sdk).
105
+ 2. Add `@kieksme/mcp-http-kit` dependency.
106
+ 3. Replace local HTTP/auth modules with `loadConfig` + `startMcpServer`.
107
+ 4. Rename Railway/Docker env vars (or keep aliases temporarily).
108
+ 5. Ensure `/health` is what Railway probes (kit default).
109
+ 6. Copy deploy templates; publish `server.json` via `mcp-publisher` if applicable.
110
+ 7. Import kit contract assertions in CI.