@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.
- package/LICENSE +191 -0
- package/README.md +1701 -0
- package/bin/orkify +3 -0
- package/boot/systemd/orkify@.service +30 -0
- package/dist/agent-name.d.ts +4 -0
- package/dist/agent-name.js +42 -0
- package/dist/alerts/AlertEvaluator.d.ts +14 -0
- package/dist/alerts/AlertEvaluator.js +135 -0
- package/dist/cli/commands/autostart.d.ts +3 -0
- package/dist/cli/commands/autostart.js +11 -0
- package/dist/cli/commands/crash-test.d.ts +3 -0
- package/dist/cli/commands/crash-test.js +17 -0
- package/dist/cli/commands/daemon-reload.d.ts +3 -0
- package/dist/cli/commands/daemon-reload.js +72 -0
- package/dist/cli/commands/delete.d.ts +3 -0
- package/dist/cli/commands/delete.js +37 -0
- package/dist/cli/commands/deploy.d.ts +6 -0
- package/dist/cli/commands/deploy.js +266 -0
- package/dist/cli/commands/down.d.ts +3 -0
- package/dist/cli/commands/down.js +36 -0
- package/dist/cli/commands/flush.d.ts +3 -0
- package/dist/cli/commands/flush.js +28 -0
- package/dist/cli/commands/kill.d.ts +3 -0
- package/dist/cli/commands/kill.js +35 -0
- package/dist/cli/commands/list.d.ts +14 -0
- package/dist/cli/commands/list.js +361 -0
- package/dist/cli/commands/logs.d.ts +3 -0
- package/dist/cli/commands/logs.js +107 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.js +151 -0
- package/dist/cli/commands/reload.d.ts +3 -0
- package/dist/cli/commands/reload.js +54 -0
- package/dist/cli/commands/restart.d.ts +3 -0
- package/dist/cli/commands/restart.js +43 -0
- package/dist/cli/commands/restore.d.ts +3 -0
- package/dist/cli/commands/restore.js +88 -0
- package/dist/cli/commands/run.d.ts +8 -0
- package/dist/cli/commands/run.js +212 -0
- package/dist/cli/commands/snap.d.ts +3 -0
- package/dist/cli/commands/snap.js +30 -0
- package/dist/cli/commands/up.d.ts +3 -0
- package/dist/cli/commands/up.js +125 -0
- package/dist/cli/crash-recovery.d.ts +2 -0
- package/dist/cli/crash-recovery.js +67 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +46 -0
- package/dist/cli/parse.d.ts +28 -0
- package/dist/cli/parse.js +97 -0
- package/dist/cluster/ClusterWrapper.d.ts +18 -0
- package/dist/cluster/ClusterWrapper.js +602 -0
- package/dist/config/ConfigStore.d.ts +11 -0
- package/dist/config/ConfigStore.js +21 -0
- package/dist/config/schema.d.ts +103 -0
- package/dist/config/schema.js +49 -0
- package/dist/constants.d.ts +83 -0
- package/dist/constants.js +289 -0
- package/dist/cron/CronScheduler.d.ts +25 -0
- package/dist/cron/CronScheduler.js +149 -0
- package/dist/daemon/GracefulManager.d.ts +8 -0
- package/dist/daemon/GracefulManager.js +29 -0
- package/dist/daemon/ManagedProcess.d.ts +71 -0
- package/dist/daemon/ManagedProcess.js +1020 -0
- package/dist/daemon/Orchestrator.d.ts +51 -0
- package/dist/daemon/Orchestrator.js +416 -0
- package/dist/daemon/RotatingWriter.d.ts +27 -0
- package/dist/daemon/RotatingWriter.js +264 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +106 -0
- package/dist/daemon/startDaemon.d.ts +30 -0
- package/dist/daemon/startDaemon.js +693 -0
- package/dist/deploy/CommandPoller.d.ts +13 -0
- package/dist/deploy/CommandPoller.js +53 -0
- package/dist/deploy/DeployExecutor.d.ts +33 -0
- package/dist/deploy/DeployExecutor.js +340 -0
- package/dist/deploy/config.d.ts +20 -0
- package/dist/deploy/config.js +161 -0
- package/dist/deploy/env.d.ts +2 -0
- package/dist/deploy/env.js +17 -0
- package/dist/deploy/tarball.d.ts +32 -0
- package/dist/deploy/tarball.js +243 -0
- package/dist/detect/framework.d.ts +2 -0
- package/dist/detect/framework.js +24 -0
- package/dist/ipc/DaemonClient.d.ts +31 -0
- package/dist/ipc/DaemonClient.js +248 -0
- package/dist/ipc/DaemonServer.d.ts +28 -0
- package/dist/ipc/DaemonServer.js +166 -0
- package/dist/ipc/MultiUserClient.d.ts +27 -0
- package/dist/ipc/MultiUserClient.js +203 -0
- package/dist/ipc/protocol.d.ts +7 -0
- package/dist/ipc/protocol.js +53 -0
- package/dist/ipc/restoreDaemon.d.ts +8 -0
- package/dist/ipc/restoreDaemon.js +19 -0
- package/dist/machine-id.d.ts +11 -0
- package/dist/machine-id.js +51 -0
- package/dist/mcp/auth.d.ts +118 -0
- package/dist/mcp/auth.js +245 -0
- package/dist/mcp/http.d.ts +20 -0
- package/dist/mcp/http.js +229 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/server.d.ts +37 -0
- package/dist/mcp/server.js +413 -0
- package/dist/probe/compute-fingerprint.d.ts +27 -0
- package/dist/probe/compute-fingerprint.js +65 -0
- package/dist/probe/parse-frames.d.ts +21 -0
- package/dist/probe/parse-frames.js +57 -0
- package/dist/probe/resolve-sourcemaps.d.ts +25 -0
- package/dist/probe/resolve-sourcemaps.js +281 -0
- package/dist/state/StateStore.d.ts +11 -0
- package/dist/state/StateStore.js +78 -0
- package/dist/telemetry/TelemetryReporter.d.ts +49 -0
- package/dist/telemetry/TelemetryReporter.js +451 -0
- package/dist/types/index.d.ts +373 -0
- package/dist/types/index.js +2 -0
- package/package.json +148 -0
- package/packages/cache/README.md +114 -0
- package/packages/cache/dist/CacheClient.d.ts +26 -0
- package/packages/cache/dist/CacheClient.d.ts.map +1 -0
- package/packages/cache/dist/CacheClient.js +174 -0
- package/packages/cache/dist/CacheClient.js.map +1 -0
- package/packages/cache/dist/CacheFileStore.d.ts +45 -0
- package/packages/cache/dist/CacheFileStore.d.ts.map +1 -0
- package/packages/cache/dist/CacheFileStore.js +446 -0
- package/packages/cache/dist/CacheFileStore.js.map +1 -0
- package/packages/cache/dist/CachePersistence.d.ts +9 -0
- package/packages/cache/dist/CachePersistence.d.ts.map +1 -0
- package/packages/cache/dist/CachePersistence.js +67 -0
- package/packages/cache/dist/CachePersistence.js.map +1 -0
- package/packages/cache/dist/CachePrimary.d.ts +25 -0
- package/packages/cache/dist/CachePrimary.d.ts.map +1 -0
- package/packages/cache/dist/CachePrimary.js +155 -0
- package/packages/cache/dist/CachePrimary.js.map +1 -0
- package/packages/cache/dist/CacheStore.d.ts +50 -0
- package/packages/cache/dist/CacheStore.d.ts.map +1 -0
- package/packages/cache/dist/CacheStore.js +271 -0
- package/packages/cache/dist/CacheStore.js.map +1 -0
- package/packages/cache/dist/constants.d.ts +6 -0
- package/packages/cache/dist/constants.d.ts.map +1 -0
- package/packages/cache/dist/constants.js +9 -0
- package/packages/cache/dist/constants.js.map +1 -0
- package/packages/cache/dist/index.d.ts +16 -0
- package/packages/cache/dist/index.d.ts.map +1 -0
- package/packages/cache/dist/index.js +86 -0
- package/packages/cache/dist/index.js.map +1 -0
- package/packages/cache/dist/serialize.d.ts +9 -0
- package/packages/cache/dist/serialize.d.ts.map +1 -0
- package/packages/cache/dist/serialize.js +40 -0
- package/packages/cache/dist/serialize.js.map +1 -0
- package/packages/cache/dist/types.d.ts +123 -0
- package/packages/cache/dist/types.d.ts.map +1 -0
- package/packages/cache/dist/types.js +2 -0
- package/packages/cache/dist/types.js.map +1 -0
- package/packages/cache/package.json +27 -0
- package/packages/cache/src/CacheClient.ts +227 -0
- package/packages/cache/src/CacheFileStore.ts +528 -0
- package/packages/cache/src/CachePersistence.ts +89 -0
- package/packages/cache/src/CachePrimary.ts +172 -0
- package/packages/cache/src/CacheStore.ts +308 -0
- package/packages/cache/src/constants.ts +10 -0
- package/packages/cache/src/index.ts +100 -0
- package/packages/cache/src/serialize.ts +49 -0
- package/packages/cache/src/types.ts +156 -0
- package/packages/cache/tsconfig.json +18 -0
- package/packages/cache/tsconfig.tsbuildinfo +1 -0
- package/packages/next/README.md +166 -0
- package/packages/next/dist/error-capture.d.ts +34 -0
- package/packages/next/dist/error-capture.d.ts.map +1 -0
- package/packages/next/dist/error-capture.js +130 -0
- package/packages/next/dist/error-capture.js.map +1 -0
- package/packages/next/dist/error-handler.d.ts +10 -0
- package/packages/next/dist/error-handler.d.ts.map +1 -0
- package/packages/next/dist/error-handler.js +186 -0
- package/packages/next/dist/error-handler.js.map +1 -0
- package/packages/next/dist/isr-cache.d.ts +9 -0
- package/packages/next/dist/isr-cache.d.ts.map +1 -0
- package/packages/next/dist/isr-cache.js +86 -0
- package/packages/next/dist/isr-cache.js.map +1 -0
- package/packages/next/dist/stream.d.ts +5 -0
- package/packages/next/dist/stream.d.ts.map +1 -0
- package/packages/next/dist/stream.js +22 -0
- package/packages/next/dist/stream.js.map +1 -0
- package/packages/next/dist/types.d.ts +33 -0
- package/packages/next/dist/types.d.ts.map +1 -0
- package/packages/next/dist/types.js +6 -0
- package/packages/next/dist/types.js.map +1 -0
- package/packages/next/dist/use-cache.d.ts +4 -0
- package/packages/next/dist/use-cache.d.ts.map +1 -0
- package/packages/next/dist/use-cache.js +86 -0
- package/packages/next/dist/use-cache.js.map +1 -0
- package/packages/next/dist/utils.d.ts +32 -0
- package/packages/next/dist/utils.d.ts.map +1 -0
- package/packages/next/dist/utils.js +88 -0
- package/packages/next/dist/utils.js.map +1 -0
- package/packages/next/package.json +52 -0
- package/packages/next/src/error-capture.ts +177 -0
- package/packages/next/src/error-handler.ts +221 -0
- package/packages/next/src/isr-cache.ts +100 -0
- package/packages/next/src/stream.ts +23 -0
- package/packages/next/src/types.ts +33 -0
- package/packages/next/src/use-cache.ts +99 -0
- package/packages/next/src/utils.ts +102 -0
- package/packages/next/tsconfig.json +19 -0
- package/packages/next/tsconfig.tsbuildinfo +1 -0
package/dist/mcp/auth.js
ADDED
|
@@ -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
|
package/dist/mcp/http.js
ADDED
|
@@ -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,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
|