@orkify/cli 1.0.0-beta.5

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 (203) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +1701 -0
  3. package/bin/orkify +3 -0
  4. package/boot/systemd/orkify@.service +30 -0
  5. package/dist/agent-name.d.ts +4 -0
  6. package/dist/agent-name.js +42 -0
  7. package/dist/alerts/AlertEvaluator.d.ts +14 -0
  8. package/dist/alerts/AlertEvaluator.js +135 -0
  9. package/dist/cli/commands/autostart.d.ts +3 -0
  10. package/dist/cli/commands/autostart.js +11 -0
  11. package/dist/cli/commands/crash-test.d.ts +3 -0
  12. package/dist/cli/commands/crash-test.js +17 -0
  13. package/dist/cli/commands/daemon-reload.d.ts +3 -0
  14. package/dist/cli/commands/daemon-reload.js +72 -0
  15. package/dist/cli/commands/delete.d.ts +3 -0
  16. package/dist/cli/commands/delete.js +37 -0
  17. package/dist/cli/commands/deploy.d.ts +6 -0
  18. package/dist/cli/commands/deploy.js +266 -0
  19. package/dist/cli/commands/down.d.ts +3 -0
  20. package/dist/cli/commands/down.js +36 -0
  21. package/dist/cli/commands/flush.d.ts +3 -0
  22. package/dist/cli/commands/flush.js +28 -0
  23. package/dist/cli/commands/kill.d.ts +3 -0
  24. package/dist/cli/commands/kill.js +35 -0
  25. package/dist/cli/commands/list.d.ts +14 -0
  26. package/dist/cli/commands/list.js +361 -0
  27. package/dist/cli/commands/logs.d.ts +3 -0
  28. package/dist/cli/commands/logs.js +107 -0
  29. package/dist/cli/commands/mcp.d.ts +3 -0
  30. package/dist/cli/commands/mcp.js +151 -0
  31. package/dist/cli/commands/reload.d.ts +3 -0
  32. package/dist/cli/commands/reload.js +54 -0
  33. package/dist/cli/commands/restart.d.ts +3 -0
  34. package/dist/cli/commands/restart.js +43 -0
  35. package/dist/cli/commands/restore.d.ts +3 -0
  36. package/dist/cli/commands/restore.js +88 -0
  37. package/dist/cli/commands/run.d.ts +8 -0
  38. package/dist/cli/commands/run.js +212 -0
  39. package/dist/cli/commands/snap.d.ts +3 -0
  40. package/dist/cli/commands/snap.js +30 -0
  41. package/dist/cli/commands/up.d.ts +3 -0
  42. package/dist/cli/commands/up.js +125 -0
  43. package/dist/cli/crash-recovery.d.ts +2 -0
  44. package/dist/cli/crash-recovery.js +67 -0
  45. package/dist/cli/index.d.ts +3 -0
  46. package/dist/cli/index.js +46 -0
  47. package/dist/cli/parse.d.ts +28 -0
  48. package/dist/cli/parse.js +97 -0
  49. package/dist/cluster/ClusterWrapper.d.ts +18 -0
  50. package/dist/cluster/ClusterWrapper.js +602 -0
  51. package/dist/config/ConfigStore.d.ts +11 -0
  52. package/dist/config/ConfigStore.js +21 -0
  53. package/dist/config/schema.d.ts +103 -0
  54. package/dist/config/schema.js +49 -0
  55. package/dist/constants.d.ts +83 -0
  56. package/dist/constants.js +289 -0
  57. package/dist/cron/CronScheduler.d.ts +25 -0
  58. package/dist/cron/CronScheduler.js +149 -0
  59. package/dist/daemon/GracefulManager.d.ts +8 -0
  60. package/dist/daemon/GracefulManager.js +29 -0
  61. package/dist/daemon/ManagedProcess.d.ts +71 -0
  62. package/dist/daemon/ManagedProcess.js +1020 -0
  63. package/dist/daemon/Orchestrator.d.ts +51 -0
  64. package/dist/daemon/Orchestrator.js +416 -0
  65. package/dist/daemon/RotatingWriter.d.ts +27 -0
  66. package/dist/daemon/RotatingWriter.js +264 -0
  67. package/dist/daemon/index.d.ts +2 -0
  68. package/dist/daemon/index.js +106 -0
  69. package/dist/daemon/startDaemon.d.ts +30 -0
  70. package/dist/daemon/startDaemon.js +693 -0
  71. package/dist/deploy/CommandPoller.d.ts +13 -0
  72. package/dist/deploy/CommandPoller.js +53 -0
  73. package/dist/deploy/DeployExecutor.d.ts +33 -0
  74. package/dist/deploy/DeployExecutor.js +340 -0
  75. package/dist/deploy/config.d.ts +20 -0
  76. package/dist/deploy/config.js +161 -0
  77. package/dist/deploy/env.d.ts +2 -0
  78. package/dist/deploy/env.js +17 -0
  79. package/dist/deploy/tarball.d.ts +32 -0
  80. package/dist/deploy/tarball.js +243 -0
  81. package/dist/detect/framework.d.ts +2 -0
  82. package/dist/detect/framework.js +24 -0
  83. package/dist/ipc/DaemonClient.d.ts +31 -0
  84. package/dist/ipc/DaemonClient.js +248 -0
  85. package/dist/ipc/DaemonServer.d.ts +28 -0
  86. package/dist/ipc/DaemonServer.js +166 -0
  87. package/dist/ipc/MultiUserClient.d.ts +27 -0
  88. package/dist/ipc/MultiUserClient.js +203 -0
  89. package/dist/ipc/protocol.d.ts +7 -0
  90. package/dist/ipc/protocol.js +53 -0
  91. package/dist/ipc/restoreDaemon.d.ts +8 -0
  92. package/dist/ipc/restoreDaemon.js +19 -0
  93. package/dist/machine-id.d.ts +11 -0
  94. package/dist/machine-id.js +51 -0
  95. package/dist/mcp/auth.d.ts +118 -0
  96. package/dist/mcp/auth.js +245 -0
  97. package/dist/mcp/http.d.ts +20 -0
  98. package/dist/mcp/http.js +229 -0
  99. package/dist/mcp/index.d.ts +3 -0
  100. package/dist/mcp/index.js +8 -0
  101. package/dist/mcp/server.d.ts +37 -0
  102. package/dist/mcp/server.js +413 -0
  103. package/dist/probe/compute-fingerprint.d.ts +27 -0
  104. package/dist/probe/compute-fingerprint.js +65 -0
  105. package/dist/probe/parse-frames.d.ts +21 -0
  106. package/dist/probe/parse-frames.js +57 -0
  107. package/dist/probe/resolve-sourcemaps.d.ts +25 -0
  108. package/dist/probe/resolve-sourcemaps.js +281 -0
  109. package/dist/state/StateStore.d.ts +11 -0
  110. package/dist/state/StateStore.js +78 -0
  111. package/dist/telemetry/TelemetryReporter.d.ts +49 -0
  112. package/dist/telemetry/TelemetryReporter.js +451 -0
  113. package/dist/types/index.d.ts +373 -0
  114. package/dist/types/index.js +2 -0
  115. package/package.json +148 -0
  116. package/packages/cache/README.md +114 -0
  117. package/packages/cache/dist/CacheClient.d.ts +26 -0
  118. package/packages/cache/dist/CacheClient.d.ts.map +1 -0
  119. package/packages/cache/dist/CacheClient.js +174 -0
  120. package/packages/cache/dist/CacheClient.js.map +1 -0
  121. package/packages/cache/dist/CacheFileStore.d.ts +45 -0
  122. package/packages/cache/dist/CacheFileStore.d.ts.map +1 -0
  123. package/packages/cache/dist/CacheFileStore.js +446 -0
  124. package/packages/cache/dist/CacheFileStore.js.map +1 -0
  125. package/packages/cache/dist/CachePersistence.d.ts +9 -0
  126. package/packages/cache/dist/CachePersistence.d.ts.map +1 -0
  127. package/packages/cache/dist/CachePersistence.js +67 -0
  128. package/packages/cache/dist/CachePersistence.js.map +1 -0
  129. package/packages/cache/dist/CachePrimary.d.ts +25 -0
  130. package/packages/cache/dist/CachePrimary.d.ts.map +1 -0
  131. package/packages/cache/dist/CachePrimary.js +155 -0
  132. package/packages/cache/dist/CachePrimary.js.map +1 -0
  133. package/packages/cache/dist/CacheStore.d.ts +50 -0
  134. package/packages/cache/dist/CacheStore.d.ts.map +1 -0
  135. package/packages/cache/dist/CacheStore.js +271 -0
  136. package/packages/cache/dist/CacheStore.js.map +1 -0
  137. package/packages/cache/dist/constants.d.ts +6 -0
  138. package/packages/cache/dist/constants.d.ts.map +1 -0
  139. package/packages/cache/dist/constants.js +9 -0
  140. package/packages/cache/dist/constants.js.map +1 -0
  141. package/packages/cache/dist/index.d.ts +16 -0
  142. package/packages/cache/dist/index.d.ts.map +1 -0
  143. package/packages/cache/dist/index.js +86 -0
  144. package/packages/cache/dist/index.js.map +1 -0
  145. package/packages/cache/dist/serialize.d.ts +9 -0
  146. package/packages/cache/dist/serialize.d.ts.map +1 -0
  147. package/packages/cache/dist/serialize.js +40 -0
  148. package/packages/cache/dist/serialize.js.map +1 -0
  149. package/packages/cache/dist/types.d.ts +123 -0
  150. package/packages/cache/dist/types.d.ts.map +1 -0
  151. package/packages/cache/dist/types.js +2 -0
  152. package/packages/cache/dist/types.js.map +1 -0
  153. package/packages/cache/package.json +27 -0
  154. package/packages/cache/src/CacheClient.ts +227 -0
  155. package/packages/cache/src/CacheFileStore.ts +528 -0
  156. package/packages/cache/src/CachePersistence.ts +89 -0
  157. package/packages/cache/src/CachePrimary.ts +172 -0
  158. package/packages/cache/src/CacheStore.ts +308 -0
  159. package/packages/cache/src/constants.ts +10 -0
  160. package/packages/cache/src/index.ts +100 -0
  161. package/packages/cache/src/serialize.ts +49 -0
  162. package/packages/cache/src/types.ts +156 -0
  163. package/packages/cache/tsconfig.json +18 -0
  164. package/packages/cache/tsconfig.tsbuildinfo +1 -0
  165. package/packages/next/README.md +166 -0
  166. package/packages/next/dist/error-capture.d.ts +34 -0
  167. package/packages/next/dist/error-capture.d.ts.map +1 -0
  168. package/packages/next/dist/error-capture.js +130 -0
  169. package/packages/next/dist/error-capture.js.map +1 -0
  170. package/packages/next/dist/error-handler.d.ts +10 -0
  171. package/packages/next/dist/error-handler.d.ts.map +1 -0
  172. package/packages/next/dist/error-handler.js +186 -0
  173. package/packages/next/dist/error-handler.js.map +1 -0
  174. package/packages/next/dist/isr-cache.d.ts +9 -0
  175. package/packages/next/dist/isr-cache.d.ts.map +1 -0
  176. package/packages/next/dist/isr-cache.js +86 -0
  177. package/packages/next/dist/isr-cache.js.map +1 -0
  178. package/packages/next/dist/stream.d.ts +5 -0
  179. package/packages/next/dist/stream.d.ts.map +1 -0
  180. package/packages/next/dist/stream.js +22 -0
  181. package/packages/next/dist/stream.js.map +1 -0
  182. package/packages/next/dist/types.d.ts +33 -0
  183. package/packages/next/dist/types.d.ts.map +1 -0
  184. package/packages/next/dist/types.js +6 -0
  185. package/packages/next/dist/types.js.map +1 -0
  186. package/packages/next/dist/use-cache.d.ts +4 -0
  187. package/packages/next/dist/use-cache.d.ts.map +1 -0
  188. package/packages/next/dist/use-cache.js +86 -0
  189. package/packages/next/dist/use-cache.js.map +1 -0
  190. package/packages/next/dist/utils.d.ts +32 -0
  191. package/packages/next/dist/utils.d.ts.map +1 -0
  192. package/packages/next/dist/utils.js +88 -0
  193. package/packages/next/dist/utils.js.map +1 -0
  194. package/packages/next/package.json +52 -0
  195. package/packages/next/src/error-capture.ts +177 -0
  196. package/packages/next/src/error-handler.ts +221 -0
  197. package/packages/next/src/isr-cache.ts +100 -0
  198. package/packages/next/src/stream.ts +23 -0
  199. package/packages/next/src/types.ts +33 -0
  200. package/packages/next/src/use-cache.ts +99 -0
  201. package/packages/next/src/utils.ts +102 -0
  202. package/packages/next/tsconfig.json +19 -0
  203. package/packages/next/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,245 @@
1
+ import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
2
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, statSync, watchFile, writeFileSync } from 'node:fs';
4
+ import { BlockList, isIPv4 } from 'node:net';
5
+ import { dirname } from 'node:path';
6
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
7
+ import { z } from 'zod';
8
+ import { MCP_CONFIG_FILE, MCP_TOKEN_PREFIX } from '../constants.js';
9
+ /**
10
+ * Valid MCP tool names — kept in sync with tools registered in server.ts.
11
+ */
12
+ export const TOOL_NAMES = [
13
+ 'list',
14
+ 'logs',
15
+ 'snap',
16
+ 'listAllUsers',
17
+ 'up',
18
+ 'down',
19
+ 'restart',
20
+ 'reload',
21
+ 'delete',
22
+ 'restore',
23
+ 'kill',
24
+ ];
25
+ // Zod schemas
26
+ const mcpKeySchema = z.object({
27
+ name: z.string().min(1),
28
+ token: z.string().startsWith(MCP_TOKEN_PREFIX),
29
+ tools: z.array(z.string()),
30
+ allowedIps: z.array(z.string()).optional(),
31
+ });
32
+ const mcpConfigSchema = z.object({
33
+ keys: z.array(mcpKeySchema).default([]),
34
+ });
35
+ // In-memory config cache
36
+ let cachedConfig = null;
37
+ let watching = false;
38
+ /**
39
+ * Load and validate the MCP config from ~/.orkify/mcp.yml.
40
+ * Returns cached version if available; cache is invalidated on SIGHUP or file change.
41
+ */
42
+ export function loadMcpConfig(configPath = MCP_CONFIG_FILE) {
43
+ if (cachedConfig && configPath === MCP_CONFIG_FILE)
44
+ return cachedConfig;
45
+ let raw;
46
+ try {
47
+ const content = readFileSync(configPath, 'utf8');
48
+ raw = parseYaml(content);
49
+ }
50
+ catch (err) {
51
+ if (err.code === 'ENOENT') {
52
+ const empty = { keys: [] };
53
+ if (configPath === MCP_CONFIG_FILE)
54
+ cachedConfig = empty;
55
+ return empty;
56
+ }
57
+ throw err;
58
+ }
59
+ const config = mcpConfigSchema.parse(raw);
60
+ if (configPath === MCP_CONFIG_FILE)
61
+ cachedConfig = config;
62
+ return config;
63
+ }
64
+ /**
65
+ * Start watching the config file for changes and listen for SIGHUP to reload.
66
+ * Called once when the HTTP server starts.
67
+ */
68
+ export function startConfigWatcher() {
69
+ if (watching)
70
+ return;
71
+ watching = true;
72
+ // Reload on SIGHUP
73
+ process.on('SIGHUP', () => {
74
+ cachedConfig = null;
75
+ console.error('MCP config cache cleared (SIGHUP)');
76
+ });
77
+ // Reload on file change. watchFile uses stat polling, so it works even if
78
+ // the file doesn't exist yet — the callback fires when the file is created.
79
+ watchFile(MCP_CONFIG_FILE, { interval: 2000 }, () => {
80
+ cachedConfig = null;
81
+ });
82
+ }
83
+ /**
84
+ * Token verifier that reads keys from the local YAML config.
85
+ * Implements the MCP SDK's OAuthTokenVerifier interface.
86
+ */
87
+ export class LocalConfigVerifier {
88
+ configPath;
89
+ constructor(configPath = MCP_CONFIG_FILE) {
90
+ this.configPath = configPath;
91
+ }
92
+ async verifyAccessToken(token) {
93
+ const config = loadMcpConfig(this.configPath);
94
+ const tokenBuf = Buffer.from(token);
95
+ for (const key of config.keys) {
96
+ const keyBuf = Buffer.from(key.token);
97
+ if (tokenBuf.length === keyBuf.length && timingSafeEqual(tokenBuf, keyBuf)) {
98
+ return {
99
+ token,
100
+ clientId: key.name,
101
+ scopes: key.tools,
102
+ // Static local tokens don't expire — this satisfies the AuthInfo interface.
103
+ expiresAt: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
104
+ };
105
+ }
106
+ }
107
+ throw new InvalidTokenError('Invalid or unknown token');
108
+ }
109
+ }
110
+ /**
111
+ * Token verifier that reads keys from the remote ProjectConfig via ConfigStore.
112
+ * Matches tokens by SHA-256 hash (the dashboard stores hashes, not raw tokens).
113
+ */
114
+ export class RemoteConfigVerifier {
115
+ configStore;
116
+ constructor(configStore) {
117
+ this.configStore = configStore;
118
+ }
119
+ async verifyAccessToken(token) {
120
+ const hash = createHash('sha256').update(token).digest('hex');
121
+ const hashBuf = Buffer.from(hash);
122
+ const mcpConfig = this.configStore.getMcpConfig();
123
+ for (const key of mcpConfig.keys) {
124
+ const keyBuf = Buffer.from(key.key_hash);
125
+ if (hashBuf.length === keyBuf.length && timingSafeEqual(hashBuf, keyBuf)) {
126
+ return {
127
+ token,
128
+ clientId: key.name,
129
+ scopes: key.tools,
130
+ expiresAt: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
131
+ };
132
+ }
133
+ }
134
+ throw new InvalidTokenError('Invalid or unknown token');
135
+ }
136
+ getAllowedIpsForToken(token) {
137
+ const hash = createHash('sha256').update(token).digest('hex');
138
+ const mcpConfig = this.configStore.getMcpConfig();
139
+ for (const key of mcpConfig.keys) {
140
+ if (hash === key.key_hash) {
141
+ return key.allowed_ips.length > 0 ? key.allowed_ips : undefined;
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ }
147
+ /**
148
+ * Generate a new MCP token: prefix + 48 hex chars (24 random bytes).
149
+ */
150
+ export function generateToken() {
151
+ return MCP_TOKEN_PREFIX + randomBytes(24).toString('hex');
152
+ }
153
+ /**
154
+ * Append a new key to the MCP config file.
155
+ * Creates the file with 0o600 permissions if it doesn't exist.
156
+ */
157
+ export function appendKeyToConfig(key, configPath = MCP_CONFIG_FILE) {
158
+ const dir = dirname(configPath);
159
+ if (!existsSync(dir)) {
160
+ mkdirSync(dir, { recursive: true });
161
+ }
162
+ let config;
163
+ try {
164
+ config = loadMcpConfig(configPath);
165
+ }
166
+ catch (err) {
167
+ console.error('Failed to load MCP config, starting fresh:', err.message);
168
+ config = { keys: [] };
169
+ }
170
+ config.keys.push(key);
171
+ const yaml = stringifyYaml(config);
172
+ if (!existsSync(configPath)) {
173
+ // Create with restrictive permissions (owner-only)
174
+ writeFileSync(configPath, yaml, { mode: 0o600 });
175
+ }
176
+ else {
177
+ writeFileSync(configPath, yaml);
178
+ }
179
+ // Invalidate cache so next load picks up the new key
180
+ if (configPath === MCP_CONFIG_FILE) {
181
+ cachedConfig = null;
182
+ }
183
+ }
184
+ /**
185
+ * Warn to stderr if the MCP config file has permissions more open than 0600.
186
+ * Skipped on Windows where Unix mode bits don't apply.
187
+ */
188
+ export function warnIfConfigInsecure(configPath = MCP_CONFIG_FILE) {
189
+ if (process.platform === 'win32')
190
+ return;
191
+ try {
192
+ const mode = statSync(configPath).mode & 0o777;
193
+ if (mode !== 0o600) {
194
+ console.error(`Warning: ${configPath} has mode 0${mode.toString(8)} — expected 0600. ` +
195
+ 'Other users may be able to read your MCP tokens.');
196
+ }
197
+ }
198
+ catch {
199
+ // File doesn't exist or can't stat — nothing to warn about
200
+ }
201
+ }
202
+ /**
203
+ * Strip the `::ffff:` prefix from IPv4-mapped IPv6 addresses.
204
+ * Express may report `::ffff:127.0.0.1` for IPv4 clients.
205
+ */
206
+ export function normalizeIp(ip) {
207
+ if (ip.startsWith('::ffff:')) {
208
+ const v4 = ip.slice(7);
209
+ if (isIPv4(v4))
210
+ return v4;
211
+ }
212
+ return ip;
213
+ }
214
+ /**
215
+ * Check if a client IP is allowed by the key's `allowedIps` list.
216
+ * Returns `true` if `allowedIps` is absent or empty (all IPs allowed).
217
+ * Uses Node.js `BlockList` as an allowlist — `check()` returns `true` for listed IPs.
218
+ */
219
+ export function isIpAllowed(clientIp, allowedIps) {
220
+ if (!allowedIps || allowedIps.length === 0)
221
+ return true;
222
+ const normalized = normalizeIp(clientIp);
223
+ const list = new BlockList();
224
+ for (const entry of allowedIps) {
225
+ if (entry.includes('/')) {
226
+ const [prefix, bits] = entry.split('/');
227
+ const type = isIPv4(prefix) ? 'ipv4' : 'ipv6';
228
+ list.addSubnet(prefix, Number(bits), type);
229
+ }
230
+ else {
231
+ const type = isIPv4(entry) ? 'ipv4' : 'ipv6';
232
+ list.addAddress(entry, type);
233
+ }
234
+ }
235
+ const type = isIPv4(normalized) ? 'ipv4' : 'ipv6';
236
+ return list.check(normalized, type);
237
+ }
238
+ /**
239
+ * Look up a key by name from the config.
240
+ */
241
+ export function findKeyByName(name, configPath) {
242
+ const config = configPath ? loadMcpConfig(configPath) : loadMcpConfig();
243
+ return config.keys.find((k) => k.name === name);
244
+ }
245
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,20 @@
1
+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
2
+ import { type Server } from 'node:http';
3
+ export interface HttpOptions {
4
+ port: number;
5
+ bind: string;
6
+ /** Enable CORS — "*" for any origin, a specific origin URL, or comma-separated origins. */
7
+ cors?: string;
8
+ /** Skip registering SIGTERM/SIGINT handlers (used when running inside the daemon). */
9
+ skipSignalHandlers?: boolean;
10
+ /** Custom token verifier — when provided, skips LocalConfigVerifier and local config watcher. */
11
+ tokenVerifier?: OAuthTokenVerifier;
12
+ }
13
+ export interface McpHttpServer {
14
+ /** Gracefully close all sessions and stop the HTTP server. */
15
+ shutdown(): Promise<void>;
16
+ /** The underlying Node.js HTTP server (for testing). */
17
+ server: Server;
18
+ }
19
+ export declare function startMcpHttpServer(options: HttpOptions): Promise<McpHttpServer>;
20
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1,229 @@
1
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import express from 'express';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { createServer } from 'node:http';
6
+ import { findKeyByName, isIpAllowed, LocalConfigVerifier, startConfigWatcher, } from './auth.js';
7
+ import { createMcpServer } from './server.js';
8
+ // How long a session can be idle before it's reaped (30 minutes)
9
+ const SESSION_TTL_MS = 30 * 60 * 1000;
10
+ // How often to check for expired sessions (5 minutes)
11
+ const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
12
+ export async function startMcpHttpServer(options) {
13
+ const verifier = options.tokenVerifier ??
14
+ (() => {
15
+ startConfigWatcher();
16
+ return new LocalConfigVerifier();
17
+ })();
18
+ const app = express();
19
+ app.use(express.json());
20
+ // CORS middleware — must run before auth so OPTIONS preflights aren't rejected with 401
21
+ if (options.cors) {
22
+ const raw = options.cors;
23
+ const isWildcard = raw === '*';
24
+ const origins = isWildcard ? [] : raw.split(',').map((o) => o.trim());
25
+ const isMulti = origins.length > 1;
26
+ app.use('/mcp', (req, res, next) => {
27
+ if (isWildcard) {
28
+ res.header('Access-Control-Allow-Origin', '*');
29
+ }
30
+ else if (isMulti) {
31
+ const reqOrigin = req.headers.origin;
32
+ if (reqOrigin && origins.includes(reqOrigin)) {
33
+ res.header('Access-Control-Allow-Origin', reqOrigin);
34
+ }
35
+ res.header('Vary', 'Origin');
36
+ }
37
+ else {
38
+ // Single origin — always echo (backward compat)
39
+ res.header('Access-Control-Allow-Origin', origins[0]);
40
+ res.header('Vary', 'Origin');
41
+ }
42
+ res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
43
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Accept');
44
+ res.header('Access-Control-Expose-Headers', 'Mcp-Session-Id');
45
+ if (req.method === 'OPTIONS') {
46
+ res.header('Access-Control-Max-Age', '86400');
47
+ res.status(204).end();
48
+ return;
49
+ }
50
+ next();
51
+ });
52
+ }
53
+ // Auth middleware on the /mcp endpoint
54
+ app.use('/mcp', requireBearerAuth({ verifier }));
55
+ // IP allowlist middleware — runs after auth, before route handlers
56
+ const isRemoteVerifier = 'getAllowedIpsForToken' in verifier;
57
+ app.use('/mcp', (req, res, next) => {
58
+ // Skip for OPTIONS (no auth on preflights — they're handled by CORS above)
59
+ if (req.method === 'OPTIONS') {
60
+ next();
61
+ return;
62
+ }
63
+ if (isRemoteVerifier) {
64
+ // Remote verifier: look up allowed IPs by token hash
65
+ const token = req.auth?.token;
66
+ if (token) {
67
+ const allowedIps = verifier.getAllowedIpsForToken(token);
68
+ if (allowedIps && allowedIps.length > 0) {
69
+ const clientIp = req.ip || req.socket.remoteAddress || '';
70
+ if (!isIpAllowed(clientIp, allowedIps)) {
71
+ res.status(403).json({ error: 'IP address not allowed for this key' });
72
+ return;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ else {
78
+ // Local verifier: look up allowed IPs by key name from YAML config
79
+ const clientId = req.auth?.clientId;
80
+ if (clientId) {
81
+ const key = findKeyByName(clientId);
82
+ if (key?.allowedIps && key.allowedIps.length > 0) {
83
+ const clientIp = req.ip || req.socket.remoteAddress || '';
84
+ if (!isIpAllowed(clientIp, key.allowedIps)) {
85
+ res.status(403).json({ error: 'IP address not allowed for this key' });
86
+ return;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ next();
92
+ });
93
+ // Session management
94
+ const transports = new Map();
95
+ const lastActivity = new Map();
96
+ // Maps session ID → clientId of the token that created it.
97
+ // Prevents a different key from hijacking another key's session.
98
+ const sessionOwners = new Map();
99
+ function touchSession(sessionId) {
100
+ lastActivity.set(sessionId, Date.now());
101
+ }
102
+ function removeSession(sessionId) {
103
+ transports.delete(sessionId);
104
+ lastActivity.delete(sessionId);
105
+ sessionOwners.delete(sessionId);
106
+ }
107
+ // Periodic sweep of idle sessions
108
+ const sweepTimer = setInterval(() => {
109
+ const cutoff = Date.now() - SESSION_TTL_MS;
110
+ for (const [sessionId, ts] of lastActivity) {
111
+ if (ts < cutoff) {
112
+ const transport = transports.get(sessionId);
113
+ if (transport)
114
+ transport.close().catch(() => { });
115
+ removeSession(sessionId);
116
+ }
117
+ }
118
+ }, SWEEP_INTERVAL_MS);
119
+ sweepTimer.unref();
120
+ // POST /mcp — initialize new sessions and handle messages
121
+ app.post('/mcp', async (req, res) => {
122
+ const sessionId = req.headers['mcp-session-id'];
123
+ if (sessionId) {
124
+ const existing = transports.get(sessionId);
125
+ if (existing) {
126
+ const owner = sessionOwners.get(sessionId);
127
+ if (owner && req.auth?.clientId !== owner) {
128
+ res.status(403).json({ error: 'Session belongs to a different key' });
129
+ return;
130
+ }
131
+ touchSession(sessionId);
132
+ await existing.handleRequest(req, res, req.body);
133
+ return;
134
+ }
135
+ }
136
+ // New session: create transport + server
137
+ const transport = new StreamableHTTPServerTransport({
138
+ sessionIdGenerator: () => randomUUID(),
139
+ });
140
+ const server = createMcpServer({ authInfo: req.auth });
141
+ transport.onclose = () => {
142
+ if (transport.sessionId)
143
+ removeSession(transport.sessionId);
144
+ };
145
+ await server.connect(transport);
146
+ await transport.handleRequest(req, res, req.body);
147
+ if (transport.sessionId) {
148
+ transports.set(transport.sessionId, transport);
149
+ if (req.auth?.clientId) {
150
+ sessionOwners.set(transport.sessionId, req.auth.clientId);
151
+ }
152
+ touchSession(transport.sessionId);
153
+ }
154
+ });
155
+ // GET /mcp — SSE stream for server-initiated messages
156
+ app.get('/mcp', async (req, res) => {
157
+ const sessionId = req.headers['mcp-session-id'];
158
+ if (!sessionId) {
159
+ res.status(400).json({ error: 'Missing Mcp-Session-Id header' });
160
+ return;
161
+ }
162
+ const transport = transports.get(sessionId);
163
+ if (!transport) {
164
+ res.status(404).json({ error: 'Unknown session' });
165
+ return;
166
+ }
167
+ const owner = sessionOwners.get(sessionId);
168
+ if (owner && req.auth?.clientId !== owner) {
169
+ res.status(403).json({ error: 'Session belongs to a different key' });
170
+ return;
171
+ }
172
+ touchSession(sessionId);
173
+ await transport.handleRequest(req, res);
174
+ });
175
+ // DELETE /mcp — session termination
176
+ app.delete('/mcp', async (req, res) => {
177
+ const sessionId = req.headers['mcp-session-id'];
178
+ if (sessionId) {
179
+ const owner = sessionOwners.get(sessionId);
180
+ if (owner && req.auth?.clientId !== owner) {
181
+ res.status(403).json({ error: 'Session belongs to a different key' });
182
+ return;
183
+ }
184
+ const transport = transports.get(sessionId);
185
+ if (transport) {
186
+ await transport.close();
187
+ removeSession(sessionId);
188
+ }
189
+ }
190
+ res.status(200).end();
191
+ });
192
+ const httpServer = createServer(app);
193
+ // Wait for the server to actually be listening (surfaces bind errors like port conflicts)
194
+ await new Promise((resolve, reject) => {
195
+ httpServer.once('error', reject);
196
+ httpServer.listen(options.port, options.bind, () => {
197
+ httpServer.removeListener('error', reject);
198
+ console.error(`MCP HTTP server listening on http://${options.bind}:${options.port}/mcp`);
199
+ resolve();
200
+ });
201
+ });
202
+ async function shutdown() {
203
+ clearInterval(sweepTimer);
204
+ // Close all active transports
205
+ const closePromises = [];
206
+ for (const [, transport] of transports) {
207
+ closePromises.push(transport.close().catch(() => { }));
208
+ }
209
+ await Promise.all(closePromises);
210
+ transports.clear();
211
+ lastActivity.clear();
212
+ sessionOwners.clear();
213
+ // Stop accepting new connections and close the server
214
+ await new Promise((resolve) => {
215
+ httpServer.close(() => resolve());
216
+ });
217
+ }
218
+ // Graceful shutdown on signals (skip when running inside the daemon)
219
+ if (!options.skipSignalHandlers) {
220
+ const onSignal = async () => {
221
+ await shutdown();
222
+ process.exit(0);
223
+ };
224
+ process.once('SIGTERM', onSignal);
225
+ process.once('SIGINT', onSignal);
226
+ }
227
+ return { shutdown, server: httpServer };
228
+ }
229
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from './server.js';
3
+ startMcpServer().catch((err) => {
4
+ // Use stderr for errors (stdout is reserved for MCP protocol)
5
+ console.error('MCP server error:', err);
6
+ process.exit(1);
7
+ });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,37 @@
1
+ import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ /**
4
+ * Format error response for AI consumption
5
+ */
6
+ declare function formatError(error: string, code?: string, context?: Record<string, unknown>): {
7
+ content: Array<{
8
+ type: 'text';
9
+ text: string;
10
+ }>;
11
+ isError: true;
12
+ };
13
+ /**
14
+ * Check if a tool is accessible given the current auth context.
15
+ * - No authInfo (stdio mode): always allowed
16
+ * - Scopes include "*": all tools allowed
17
+ * - Otherwise: tool name must be in scopes
18
+ */
19
+ export declare function checkToolAccess(toolName: string, authInfo?: AuthInfo): {
20
+ allowed: false;
21
+ error: ReturnType<typeof formatError>;
22
+ } | {
23
+ allowed: true;
24
+ };
25
+ /**
26
+ * Create and configure the MCP server with all ORKIFY tools.
27
+ * When authInfo is provided (HTTP mode), per-tool scope checks are enforced.
28
+ */
29
+ export declare function createMcpServer(options?: {
30
+ authInfo?: AuthInfo;
31
+ }): McpServer;
32
+ /**
33
+ * Start the MCP server with stdio transport
34
+ */
35
+ export declare function startMcpServer(): Promise<void>;
36
+ export {};
37
+ //# sourceMappingURL=server.d.ts.map