melchizedek-agents 1.1.0 → 1.3.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.
- package/.env.example +19 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +373 -0
- package/DOCUMENTATION.md +193 -21
- package/ONBOARDING.md +378 -0
- package/QUICKSTART.md +15 -2
- package/README.md +26 -3
- package/config/agents/examples/connectors.yaml +82 -0
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/critic.yaml +8 -8
- package/config/agents/examples/image_production.yaml +4 -3
- package/config/agents/syndicate.schema.json +389 -33
- package/config/agents/syndicateSchema.yaml +18 -6
- package/config/agents/templates/intake_extractor.yaml +3 -2
- package/config/agents/templates/systems_operator.yaml +5 -2
- package/dist/lib/a2a/app.d.ts +12 -0
- package/dist/lib/a2a/app.js +25 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +77 -8
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/chatgpt/adapter.d.ts +50 -0
- package/dist/lib/chatgpt/adapter.js +107 -0
- package/dist/lib/chatgpt/cli.d.ts +23 -0
- package/dist/lib/chatgpt/cli.js +77 -0
- package/dist/lib/chatgpt/oauth.d.ts +103 -0
- package/dist/lib/chatgpt/oauth.js +448 -0
- package/dist/lib/chatgpt/state.d.ts +98 -0
- package/dist/lib/chatgpt/state.js +145 -0
- package/dist/lib/chatgpt/store.d.ts +24 -0
- package/dist/lib/chatgpt/store.js +94 -0
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +97 -62
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/dispatch.js +6 -2
- package/dist/lib/doctor.d.ts +107 -0
- package/dist/lib/doctor.js +217 -7
- package/dist/lib/loadSyndicate.d.ts +15 -0
- package/dist/lib/mcp/server.d.ts +195 -0
- package/dist/lib/mcp/server.js +564 -0
- package/dist/lib/memory/supabaseMemoryService.js +2 -2
- package/dist/lib/models/adapterResolver.js +6 -1
- package/dist/lib/models/capabilities.d.ts +9 -0
- package/dist/lib/models/capabilities.js +24 -0
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +3 -2
- package/dist/lib/models/gateway.js +8 -4
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/onboarding.d.ts +135 -0
- package/dist/lib/onboarding.js +578 -0
- package/dist/lib/runtime/approvals.d.ts +2 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
- package/dist/lib/runtime/native/agentLoop.js +2 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
- package/dist/lib/runtime/native/checkpoint.js +13 -19
- package/dist/lib/runtime/native/delegate.d.ts +82 -1
- package/dist/lib/runtime/native/delegate.js +157 -2
- package/dist/lib/runtime/native/interrupts.d.ts +88 -5
- package/dist/lib/runtime/native/interrupts.js +220 -30
- package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
- package/dist/lib/runtime/syndicateTurn.js +324 -77
- package/dist/lib/runtime/turnControl.d.ts +41 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +1 -1
- package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
- package/dist/lib/storage/postgres/taskQueue.js +10 -3
- package/dist/lib/syndicateSchema.d.ts +184 -9
- package/dist/lib/syndicateSchema.js +143 -29
- package/dist/lib/tools/auth.d.ts +13 -1
- package/dist/lib/tools/auth.js +10 -2
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialUses.d.ts +98 -0
- package/dist/lib/tools/credentialUses.js +120 -0
- package/dist/lib/tools/generateImageTool.js +2 -1
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
- package/dist/lib/tools/mcpToolFactory.js +72 -26
- package/dist/lib/tools/mcpTransports.d.ts +12 -0
- package/dist/lib/tools/mcpTransports.js +11 -0
- package/dist/lib/tools/oauthConsent.d.ts +18 -3
- package/dist/lib/tools/oauthConsent.js +57 -15
- package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
- package/dist/lib/tools/oauthDiscovery.js +616 -0
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +54 -13
- package/dist/lib/tools/oauthTools.js +106 -23
- package/dist/lib/tools/openapiTools.d.ts +7 -0
- package/dist/lib/tools/openapiTools.js +49 -3
- package/dist/lib/tools/taskTools.d.ts +32 -2
- package/dist/lib/tools/taskTools.js +93 -33
- package/dist/lib/tools/xApiSearchTool.js +2 -2
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +10 -7
- package/dist/lib/workflow.js +10 -7
- package/dist/scripts/a2a_server.d.ts +2 -2
- package/dist/scripts/a2a_server.js +19 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +24 -5
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -4
- package/dist/scripts/mcp_server.d.ts +12 -0
- package/dist/scripts/mcp_server.js +345 -0
- package/dist/scripts/setup.d.ts +25 -0
- package/dist/scripts/setup.js +181 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/syndicate_chat.js +12 -3
- package/dist/scripts/yaml_codemod.d.ts +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-models.md +4 -2
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek-models/SKILL.md +6 -2
- package/skills/melchizedek-onboard/SKILL.md +71 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -40,15 +40,18 @@
|
|
|
40
40
|
* --agent names a subagent of the syndicate, or its orchestrator.
|
|
41
41
|
* MELCHIZEDEK_TASKS_FILE moves the store (default: outputs/tasks.json),
|
|
42
42
|
* and must match the chat process's value. Run ONE worker per store.
|
|
43
|
+
* MELCHIZEDEK_CHECKPOINT_MAX_BYTES caps a stored checkpoint (bytes of
|
|
44
|
+
* JSON, default 5 MiB); a larger one is skipped and the previous kept.
|
|
43
45
|
*/
|
|
44
46
|
import { randomUUID } from 'node:crypto';
|
|
45
47
|
import { loadEnv } from '../lib/loadEnv.js';
|
|
46
48
|
import { loadSyndicate } from '../lib/loadSyndicate.js';
|
|
47
49
|
import { runDurableTurn } from '../lib/runtime/native/checkpoint.js';
|
|
48
50
|
import { runtimeSetting } from '../lib/runtime/runtimeFlag.js';
|
|
51
|
+
import { refuseChatGptSignInOnServedSurface } from '../lib/chatgpt/state.js';
|
|
49
52
|
import { setLogLevel } from '../lib/runtime/logging.js';
|
|
50
53
|
import { PROVIDERS, providerForModel, providerKeyPresent, } from '../lib/models/registry.js';
|
|
51
|
-
import { applyInterrupted, getTaskBackend, setTaskBackend, taskStorePath } from '../lib/tools/taskTools.js';
|
|
54
|
+
import { CHECKPOINT_MAX_BYTES_ENV, applyInterrupted, checkpointMaxBytesSetting, fileTaskBackendWith, getTaskBackend, setTaskBackend, taskStorePath, } from '../lib/tools/taskTools.js';
|
|
52
55
|
import { postgresStorage } from '../lib/storage/postgres/index.js';
|
|
53
56
|
import { dbSchema } from '../lib/storage/schema.js';
|
|
54
57
|
import { hostname } from 'node:os';
|
|
@@ -56,6 +59,14 @@ loadEnv(import.meta.url);
|
|
|
56
59
|
setLogLevel('warn');
|
|
57
60
|
// MELCHIZEDEK_RUNTIME=adk (removed in 1.0.0) stops the worker here, naming the release.
|
|
58
61
|
runtimeSetting();
|
|
62
|
+
// Sign in with ChatGPT is local only (ADR 0126): the worker runs jobs unattended, so it never runs on it.
|
|
63
|
+
try {
|
|
64
|
+
refuseChatGptSignInOnServedSurface('melchizedek-worker');
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
59
70
|
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
60
71
|
const argv = process.argv.slice(2).filter((a) => a !== '--');
|
|
61
72
|
const flag = (name) => {
|
|
@@ -90,10 +101,17 @@ if (!providerKeyPresent(provider)) {
|
|
|
90
101
|
const log = (message) => console.log(`[worker] ${message}`);
|
|
91
102
|
// The queue: the JSON file, or Postgres when DATABASE_URL is set, where any
|
|
92
103
|
// number of workers may run (each claim is FOR UPDATE SKIP LOCKED).
|
|
104
|
+
// A checkpoint above MELCHIZEDEK_CHECKPOINT_MAX_BYTES (bytes of JSON, default
|
|
105
|
+
// 5 MiB) is not saved: the job keeps its previous one (ADR 0113).
|
|
106
|
+
const rawCap = process.env[CHECKPOINT_MAX_BYTES_ENV];
|
|
107
|
+
const checkpointMaxBytes = checkpointMaxBytesSetting(rawCap);
|
|
108
|
+
if (rawCap?.trim() && Number(rawCap.trim()) !== checkpointMaxBytes) {
|
|
109
|
+
log(`${CHECKPOINT_MAX_BYTES_ENV} is not a positive whole number of bytes; using ${checkpointMaxBytes}`);
|
|
110
|
+
}
|
|
111
|
+
const taskQueue = { checkpointMaxBytes, log };
|
|
93
112
|
const databaseUrl = process.env.DATABASE_URL?.trim();
|
|
94
|
-
const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema() }) : undefined;
|
|
95
|
-
|
|
96
|
-
setTaskBackend(pg.taskQueue);
|
|
113
|
+
const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema(), taskQueue }) : undefined;
|
|
114
|
+
setTaskBackend(pg ? pg.taskQueue : fileTaskBackendWith(taskQueue));
|
|
97
115
|
const backend = getTaskBackend();
|
|
98
116
|
/** A claimed job stays this worker's while it renews; the job timeout bounds a turn. */
|
|
99
117
|
const lease = { workerId: `${hostname()}:${process.pid}:${randomUUID().slice(0, 8)}`, leaseMs: 60_000 };
|
|
@@ -169,7 +187,8 @@ async function drain() {
|
|
|
169
187
|
ran += 1;
|
|
170
188
|
const started = Date.now();
|
|
171
189
|
const label = job.owner ? `${job.id} (${job.owner})` : job.id;
|
|
172
|
-
|
|
190
|
+
// The title is the person's words: the log names the job, never its text.
|
|
191
|
+
log(`${label} started (${(job.instruction ?? job.title).length.toLocaleString()} chars)`);
|
|
173
192
|
// Renew the lease while the job runs, so no other worker takes it back;
|
|
174
193
|
// a renewal that finds the claim gone (the job was cancelled) stops the run.
|
|
175
194
|
const held = job;
|
package/dist/scripts/doctor.d.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* removed that runtime). Lists the tools that need an OAuth grant (an
|
|
12
12
|
* `auth: { oauth2 }` or `mcp_auth`, ADR 0112): each user's consent, or the
|
|
13
13
|
* server's own client credentials and whether their variables are set
|
|
14
|
-
* (names only).
|
|
14
|
+
* (names only). When any A2A identity or billing variable is set, a
|
|
15
|
+
* `serving` line names the A2A_AUTH mode and A2A_KEY_MODE and what the
|
|
16
|
+
* server would refuse to start on. Closes with the env vars that would
|
|
15
17
|
* unlock the most, where to get each, and the first command to try.
|
|
16
18
|
*
|
|
17
19
|
* Read-only: never edits .env, never sends a request, never prints a key
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
* Usage:
|
|
21
23
|
* npm run doctor
|
|
22
24
|
* npm run doctor -- --json # machine-readable
|
|
23
|
-
* npm run doctor -- --check # exit 1
|
|
25
|
+
* npm run doctor -- --check # exit 1 on a blocked syndicate, a runtime problem, an unrecognised MODEL_GATEWAY, or an OAuth or credential host problem
|
|
24
26
|
* npm run doctor -- --matrix # the provider × capability matrix
|
|
25
27
|
* npm run doctor -- --fix-namespaces [file…] # give long-term syndicates a memory_namespace
|
|
26
28
|
* MELCHIZEDEK_AGENTS_DIR=/path npm run doctor
|
package/dist/scripts/doctor.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* removed that runtime). Lists the tools that need an OAuth grant (an
|
|
12
12
|
* `auth: { oauth2 }` or `mcp_auth`, ADR 0112): each user's consent, or the
|
|
13
13
|
* server's own client credentials and whether their variables are set
|
|
14
|
-
* (names only).
|
|
14
|
+
* (names only). When any A2A identity or billing variable is set, a
|
|
15
|
+
* `serving` line names the A2A_AUTH mode and A2A_KEY_MODE and what the
|
|
16
|
+
* server would refuse to start on. Closes with the env vars that would
|
|
15
17
|
* unlock the most, where to get each, and the first command to try.
|
|
16
18
|
*
|
|
17
19
|
* Read-only: never edits .env, never sends a request, never prints a key
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
* Usage:
|
|
21
23
|
* npm run doctor
|
|
22
24
|
* npm run doctor -- --json # machine-readable
|
|
23
|
-
* npm run doctor -- --check # exit 1
|
|
25
|
+
* npm run doctor -- --check # exit 1 on a blocked syndicate, a runtime problem, an unrecognised MODEL_GATEWAY, or an OAuth or credential host problem
|
|
24
26
|
* npm run doctor -- --matrix # the provider × capability matrix
|
|
25
27
|
* npm run doctor -- --fix-namespaces [file…] # give long-term syndicates a memory_namespace
|
|
26
28
|
* MELCHIZEDEK_AGENTS_DIR=/path npm run doctor
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
import fs from 'node:fs';
|
|
29
31
|
import path from 'node:path';
|
|
30
32
|
import { loadEnv } from '../lib/loadEnv.js';
|
|
31
|
-
import { renderDoctor, runDoctor } from '../lib/doctor.js';
|
|
33
|
+
import { checkProblems, renderDoctor, runDoctor } from '../lib/doctor.js';
|
|
32
34
|
import { renderCapabilityMatrix } from '../lib/models/capabilities.js';
|
|
33
35
|
import { assignMemoryNamespace, LEGACY_MEMORY_APP_NAME } from '../lib/memory/namespace.js';
|
|
34
36
|
loadEnv();
|
|
@@ -91,5 +93,5 @@ if (json) {
|
|
|
91
93
|
else {
|
|
92
94
|
console.log(renderDoctor(result, { color: !noColor }));
|
|
93
95
|
}
|
|
94
|
-
if (check && (result.
|
|
96
|
+
if (check && checkProblems(result).length)
|
|
95
97
|
process.exit(1);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface McpCliArgs {
|
|
3
|
+
syndicates: string[];
|
|
4
|
+
agentsDir?: string;
|
|
5
|
+
http: boolean;
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
help: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The bin's flags. Throws, naming the flag, on one it does not know. */
|
|
11
|
+
export declare function parseMcpArgs(argv: string[]): McpCliArgs;
|
|
12
|
+
export declare function startMcpServer(argv?: string[]): Promise<void>;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/mcp_server.ts — the `melchizedek-mcp` bin: syndicates as MCP tools
|
|
4
|
+
* (ADR 0125). Thin: it reads flags and the environment, loads the syndicates,
|
|
5
|
+
* refuses unsafe binds, and hands everything to lib/mcp/server.ts.
|
|
6
|
+
*
|
|
7
|
+
* claude mcp add melch -- npx melchizedek-mcp
|
|
8
|
+
* melchizedek-mcp --syndicate research_desk --syndicate tutor
|
|
9
|
+
* melchizedek-mcp --http --port 4100
|
|
10
|
+
*
|
|
11
|
+
* Every console line goes to stderr: on stdio, stdout is the protocol.
|
|
12
|
+
*
|
|
13
|
+
* Flags:
|
|
14
|
+
* --syndicate <id> serve this syndicate (repeatable; a bare id may also
|
|
15
|
+
* resolve to the shipped examples/ and templates/).
|
|
16
|
+
* Positional ids work too. None: every YAML at the
|
|
17
|
+
* agents directory's root.
|
|
18
|
+
* --agents-dir <dir> the agents directory (MELCHIZEDEK_AGENTS_DIR, else
|
|
19
|
+
* <cwd>/config/agents)
|
|
20
|
+
* --http Streamable HTTP at /mcp instead of stdio
|
|
21
|
+
* --host <addr> HTTP bind (MCP_HOST, default 127.0.0.1)
|
|
22
|
+
* --port <n> HTTP port (MCP_PORT, default 4100)
|
|
23
|
+
*
|
|
24
|
+
* Environment (all optional):
|
|
25
|
+
* MCP_SERVER_SECRET bearer every HTTP request presents; required, 32+
|
|
26
|
+
* characters, on a bind beyond loopback
|
|
27
|
+
* MCP_ALLOWED_HOSTS comma list: Host names a non-loopback bind answers to
|
|
28
|
+
* MCP_TRUST_PROXY Express trust proxy (hop count, true/false, subnets)
|
|
29
|
+
* MCP_RATE_LIMIT_PER_MINUTE HTTP requests per minute per IP (240)
|
|
30
|
+
* MCP_USER_ID the scope a stdio client's sessions are stored under (default)
|
|
31
|
+
* DATABASE_URL Postgres for sessions, memory, turn locks, audit, credentials
|
|
32
|
+
* A2A_TASK_TIMEOUT_MS, A2A_MAX_CONCURRENT_TASKS, A2A_MAX_CONCURRENT_PER_SCOPE,
|
|
33
|
+
* A2A_MAX_CONCURRENT_PER_CALLER, A2A_TURN_LOCK_WAIT_MS, A2A_BUDGETS,
|
|
34
|
+
* A2A_SHUTDOWN_GRACE_MS, MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI,
|
|
35
|
+
* OAUTH_CALLBACK_IDENTITY, MELCHIZEDEK_OAUTH_HOSTS, MELCHIZEDEK_CREDENTIAL_HOSTS
|
|
36
|
+
* as melchizedek-serve reads them (scripts/a2a_server.ts)
|
|
37
|
+
*/
|
|
38
|
+
import { readdirSync, realpathSync } from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { pathToFileURL } from 'node:url';
|
|
41
|
+
/** The bin's flags. Throws, naming the flag, on one it does not know. */
|
|
42
|
+
export function parseMcpArgs(argv) {
|
|
43
|
+
const out = { syndicates: [], http: false, help: false };
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
const value = () => {
|
|
47
|
+
const v = argv[++i];
|
|
48
|
+
if (v === undefined || v.startsWith('--'))
|
|
49
|
+
throw new Error(`${a} needs a value`);
|
|
50
|
+
return v;
|
|
51
|
+
};
|
|
52
|
+
if (a === '--help' || a === '-h')
|
|
53
|
+
out.help = true;
|
|
54
|
+
else if (a === '--http')
|
|
55
|
+
out.http = true;
|
|
56
|
+
else if (a === '--stdio')
|
|
57
|
+
out.http = false;
|
|
58
|
+
else if (a === '--syndicate')
|
|
59
|
+
out.syndicates.push(value());
|
|
60
|
+
else if (a === '--agents-dir')
|
|
61
|
+
out.agentsDir = value();
|
|
62
|
+
else if (a === '--host')
|
|
63
|
+
out.host = value();
|
|
64
|
+
else if (a === '--port') {
|
|
65
|
+
const n = Number(value());
|
|
66
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535)
|
|
67
|
+
throw new Error('--port must be a port number');
|
|
68
|
+
out.port = n;
|
|
69
|
+
}
|
|
70
|
+
else if (a.startsWith('-'))
|
|
71
|
+
throw new Error(`Unknown flag ${a} (see --help)`);
|
|
72
|
+
else
|
|
73
|
+
out.syndicates.push(a);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
const HELP = [
|
|
78
|
+
'Usage: melchizedek-mcp [--syndicate <id>]... [--agents-dir <dir>] [--http [--host <addr>] [--port <n>]]',
|
|
79
|
+
'',
|
|
80
|
+
'Serves syndicates as MCP tools: one tool per syndicate ({ message, session_id? }),',
|
|
81
|
+
'plus melch_resume for a turn that paused (an approval, a question, an authorization).',
|
|
82
|
+
'stdio by default (Claude Code: claude mcp add melch -- npx melchizedek-mcp);',
|
|
83
|
+
'--http serves Streamable HTTP at /mcp on 127.0.0.1; a wider --host needs MCP_SERVER_SECRET.',
|
|
84
|
+
'Environment variables are listed at the top of scripts/mcp_server.ts.',
|
|
85
|
+
].join('\n');
|
|
86
|
+
function envInt(name, fallback) {
|
|
87
|
+
const raw = process.env[name]?.trim();
|
|
88
|
+
if (!raw)
|
|
89
|
+
return fallback;
|
|
90
|
+
const n = Number(raw);
|
|
91
|
+
if (!Number.isFinite(n) || n < 0)
|
|
92
|
+
throw new Error(`${name} must be a non-negative number`);
|
|
93
|
+
return Math.floor(n);
|
|
94
|
+
}
|
|
95
|
+
function trustProxy() {
|
|
96
|
+
const raw = process.env.MCP_TRUST_PROXY?.trim();
|
|
97
|
+
if (!raw)
|
|
98
|
+
return false;
|
|
99
|
+
if (/^\d+$/.test(raw))
|
|
100
|
+
return Number(raw);
|
|
101
|
+
if (raw === 'true')
|
|
102
|
+
return true;
|
|
103
|
+
if (raw === 'false')
|
|
104
|
+
return false;
|
|
105
|
+
return raw;
|
|
106
|
+
}
|
|
107
|
+
function fatal(message) {
|
|
108
|
+
console.error(`[MCP] ✗ FATAL: ${message}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
export async function startMcpServer(argv = process.argv.slice(2)) {
|
|
112
|
+
let args;
|
|
113
|
+
try {
|
|
114
|
+
args = parseMcpArgs(argv);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
if (args.help) {
|
|
120
|
+
console.error(HELP);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
if (args.agentsDir)
|
|
124
|
+
process.env.MELCHIZEDEK_AGENTS_DIR = path.resolve(args.agentsDir);
|
|
125
|
+
const { loadEnv, isPlaceholderValue } = await import('../lib/loadEnv.js');
|
|
126
|
+
loadEnv(import.meta.url);
|
|
127
|
+
// Sign in with ChatGPT is local only (ADR 0126). Over stdio this bin is one person's process, spawned by
|
|
128
|
+
// their own MCP client; --http can be reached by others, so it refuses like melchizedek-serve.
|
|
129
|
+
if (args.http) {
|
|
130
|
+
const { refuseChatGptSignInOnServedSurface } = await import('../lib/chatgpt/state.js');
|
|
131
|
+
try {
|
|
132
|
+
refuseChatGptSignInOnServedSurface('melchizedek-mcp --http');
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (process.env.OTEL_CONSOLE_SPANS === undefined)
|
|
139
|
+
process.env.OTEL_CONSOLE_SPANS = 'false';
|
|
140
|
+
const { setLogLevel } = await import('../lib/runtime/logging.js');
|
|
141
|
+
setLogLevel('warn');
|
|
142
|
+
const log = (m) => console.error(`[MCP] ${m}`);
|
|
143
|
+
const warn = (m) => console.error(`[MCP] ⚠ ${m}`);
|
|
144
|
+
const { createMcpServer, mcpBindProblem, mcpHttpApp, serveMcpStdio, toolNameFor } = await import('../lib/mcp/server.js');
|
|
145
|
+
const { loadSyndicate, nestedLoader } = await import('../lib/loadSyndicate.js');
|
|
146
|
+
const { syndicateOAuthHostProblems } = await import('../lib/tools/oauthTools.js');
|
|
147
|
+
const { syndicateCredentialHostProblems } = await import('../lib/tools/credentialUses.js');
|
|
148
|
+
// ── Transport posture first: a refused bind loads nothing ────────────────
|
|
149
|
+
const host = args.host ?? (process.env.MCP_HOST?.trim() || '127.0.0.1');
|
|
150
|
+
const port = args.port ?? envInt('MCP_PORT', 4100);
|
|
151
|
+
let secret = process.env.MCP_SERVER_SECRET?.trim() || undefined;
|
|
152
|
+
if (secret && isPlaceholderValue(secret))
|
|
153
|
+
fatal('MCP_SERVER_SECRET is still a placeholder. Generate one: openssl rand -hex 32');
|
|
154
|
+
if (args.http) {
|
|
155
|
+
const problem = mcpBindProblem({ host, ...(secret ? { secret } : {}) });
|
|
156
|
+
if (problem)
|
|
157
|
+
fatal(problem);
|
|
158
|
+
}
|
|
159
|
+
// ── The syndicates: the exposure ──────────────────────────────────────────
|
|
160
|
+
const bindings = () => ({
|
|
161
|
+
current_date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
|
|
162
|
+
});
|
|
163
|
+
const agentsDir = path.resolve(process.env.MELCHIZEDEK_AGENTS_DIR ?? path.join(process.cwd(), 'config', 'agents'));
|
|
164
|
+
let files = args.syndicates.map((id) => (id.endsWith('.yaml') || id.endsWith('.yml') ? id : `${id}.yaml`));
|
|
165
|
+
const named = files.length > 0;
|
|
166
|
+
if (!named) {
|
|
167
|
+
try {
|
|
168
|
+
files = readdirSync(agentsDir)
|
|
169
|
+
.filter((f) => (f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
|
|
170
|
+
.sort();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
files = [];
|
|
174
|
+
}
|
|
175
|
+
if (!files.length)
|
|
176
|
+
fatal(`No syndicates at the root of ${agentsDir}. Name one: melchizedek-mcp --syndicate <id> (a shipped example works: --syndicate tutor).`);
|
|
177
|
+
}
|
|
178
|
+
const syndicates = [];
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
try {
|
|
181
|
+
const config = loadSyndicate(file, { bindings: bindings(), shippedFallback: named });
|
|
182
|
+
// The operator's host allowlists hold here as on the A2A server (ADR 0114, ADR 0122).
|
|
183
|
+
const nested = nestedLoader(config, (ref) => loadSyndicate(ref, { bindings: bindings() }));
|
|
184
|
+
const load = (ref) => {
|
|
185
|
+
try {
|
|
186
|
+
return nested(ref);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const problems = [...syndicateOAuthHostProblems([config], { load }), ...syndicateCredentialHostProblems([config], { load })];
|
|
193
|
+
if (problems.length)
|
|
194
|
+
throw new Error(`refused by the operator's host allowlists:\n - ${problems.join('\n - ')}`);
|
|
195
|
+
syndicates.push({ id: toolNameFor(path.basename(file)), config });
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
const why = err?.code === 'ENOENT' ? 'not found' : err instanceof Error ? err.message : String(err);
|
|
199
|
+
if (named)
|
|
200
|
+
fatal(`${file}: ${why}`);
|
|
201
|
+
warn(`Skipped ${file}: ${why.split('\n')[0]}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!syndicates.length)
|
|
205
|
+
fatal('No syndicate loaded.');
|
|
206
|
+
// ── Storage, budgets, tool credentials: as melchizedek-serve ─────────────
|
|
207
|
+
const databaseUrl = process.env.DATABASE_URL?.trim() || undefined;
|
|
208
|
+
let pg;
|
|
209
|
+
if (databaseUrl) {
|
|
210
|
+
const { postgresStorage } = await import('../lib/storage/postgres/index.js');
|
|
211
|
+
const { dbSchema } = await import('../lib/storage/schema.js');
|
|
212
|
+
pg = postgresStorage({
|
|
213
|
+
connectionString: databaseUrl,
|
|
214
|
+
schema: dbSchema(),
|
|
215
|
+
memory: { apiKey: process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY || '' },
|
|
216
|
+
});
|
|
217
|
+
const { compareSchema, schemaBehindMessage, shippedSchemaVersion } = await import('../lib/storage/schemaVersion.js');
|
|
218
|
+
const check = compareSchema(await pg.schemaVersion(), shippedSchemaVersion());
|
|
219
|
+
if (check.state === 'behind' && process.env.ALLOW_SCHEMA_MISMATCH?.trim().toLowerCase() !== 'true')
|
|
220
|
+
fatal(schemaBehindMessage(check));
|
|
221
|
+
}
|
|
222
|
+
let policy;
|
|
223
|
+
const budgetsJson = process.env.A2A_BUDGETS?.trim();
|
|
224
|
+
if (budgetsJson) {
|
|
225
|
+
const { budgets, memoryUsageStore, parseBudgets, postgresUsageStore } = await import('../lib/a2a/policy.js');
|
|
226
|
+
try {
|
|
227
|
+
policy = budgets(parseBudgets(budgetsJson), { store: pg ? postgresUsageStore(pg.pool) : memoryUsageStore() });
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const { oauthServerSetup, callbackIdentity } = await import('../lib/a2a/oauthSetup.js');
|
|
234
|
+
const { postgresCredentialRows } = await import('../lib/storage/postgres/credentialStore.js');
|
|
235
|
+
let oauth;
|
|
236
|
+
try {
|
|
237
|
+
oauth = oauthServerSetup({
|
|
238
|
+
configs: syndicates.map((s) => s.config),
|
|
239
|
+
load: (ref) => loadSyndicate(ref),
|
|
240
|
+
env: process.env,
|
|
241
|
+
...(pg ? { rows: postgresCredentialRows(pg.pool), audit: pg.audit } : {}),
|
|
242
|
+
allowPrivate: process.env.ALLOW_PRIVATE_OPENAPI === 'true' || process.env.ALLOW_PRIVATE_MCP === 'true',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
247
|
+
}
|
|
248
|
+
for (const w of oauth.warnings)
|
|
249
|
+
warn(w);
|
|
250
|
+
const consent = oauth.toolCredentials?.consent;
|
|
251
|
+
if (consent && !args.http) {
|
|
252
|
+
warn('stdio serves no OAuth consent callback: a consent pause completes only where OAUTH_REDIRECT_URI is served by a server sharing this credential store.');
|
|
253
|
+
}
|
|
254
|
+
const mcp = createMcpServer({
|
|
255
|
+
syndicates,
|
|
256
|
+
...(pg ? { sessionService: pg.sessionService, turnLock: pg.turnLock, audit: pg.audit } : {}),
|
|
257
|
+
...(pg?.memoryService ? { memoryService: pg.memoryService } : {}),
|
|
258
|
+
taskTimeoutMs: envInt('A2A_TASK_TIMEOUT_MS', 15 * 60 * 1000),
|
|
259
|
+
maxConcurrentTasks: envInt('A2A_MAX_CONCURRENT_TASKS', 0),
|
|
260
|
+
maxConcurrentPerScope: envInt('A2A_MAX_CONCURRENT_PER_SCOPE', 4),
|
|
261
|
+
maxConcurrentPerCaller: envInt('A2A_MAX_CONCURRENT_PER_CALLER', 0),
|
|
262
|
+
turnLockWaitMs: envInt('A2A_TURN_LOCK_WAIT_MS', 30_000),
|
|
263
|
+
...(policy ? { policy } : {}),
|
|
264
|
+
...(oauth.toolCredentials ? { toolCredentials: oauth.toolCredentials } : {}),
|
|
265
|
+
log,
|
|
266
|
+
warn,
|
|
267
|
+
});
|
|
268
|
+
const toolList = syndicates.map((s) => s.id).join(', ');
|
|
269
|
+
const graceMs = envInt('A2A_SHUTDOWN_GRACE_MS', 25_000);
|
|
270
|
+
let stopping = false;
|
|
271
|
+
let closeHttp;
|
|
272
|
+
const stop = async (why) => {
|
|
273
|
+
if (stopping)
|
|
274
|
+
return;
|
|
275
|
+
stopping = true;
|
|
276
|
+
log(`${why}: waiting up to ${graceMs} ms for running turns.`);
|
|
277
|
+
const canceled = await mcp.shutdown(graceMs);
|
|
278
|
+
if (canceled > 0)
|
|
279
|
+
warn(`${canceled} turn(s) did not finish in time and were canceled.`);
|
|
280
|
+
await closeHttp?.().catch(() => { });
|
|
281
|
+
const { flushTracing } = await import('../lib/observability/tracer.js');
|
|
282
|
+
await flushTracing().catch(() => { });
|
|
283
|
+
await pg?.close().catch(() => { });
|
|
284
|
+
process.exit(0);
|
|
285
|
+
};
|
|
286
|
+
process.on('SIGTERM', () => void stop('SIGTERM'));
|
|
287
|
+
process.on('SIGINT', () => void stop('SIGINT'));
|
|
288
|
+
if (!args.http) {
|
|
289
|
+
const scope = process.env.MCP_USER_ID?.trim() || 'default';
|
|
290
|
+
const { HEADER_VALUE_PATTERN } = await import('../lib/a2a/identity.js');
|
|
291
|
+
if (!HEADER_VALUE_PATTERN.test(scope))
|
|
292
|
+
fatal('MCP_USER_ID must match [A-Za-z0-9._-]{1,64}');
|
|
293
|
+
await serveMcpStdio(mcp, { caller: { scopeKey: scope, caller: 'local' } });
|
|
294
|
+
log(`✓ Serving ${syndicates.length} syndicate tool(s) over stdio: ${toolList}`);
|
|
295
|
+
process.stdin.on('end', () => void stop('stdin closed'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const allowedHosts = process.env.MCP_ALLOWED_HOSTS?.split(',').map((s) => s.trim()).filter(Boolean);
|
|
299
|
+
const { consentCallback } = await import('../lib/a2a/app.js');
|
|
300
|
+
const app = mcpHttpApp(mcp, {
|
|
301
|
+
host,
|
|
302
|
+
port,
|
|
303
|
+
...(secret ? { secret } : {}),
|
|
304
|
+
...(allowedHosts?.length ? { allowedHosts } : {}),
|
|
305
|
+
trustProxy: trustProxy(),
|
|
306
|
+
rateLimitPerMinute: envInt('MCP_RATE_LIMIT_PER_MINUTE', 240),
|
|
307
|
+
...(consent
|
|
308
|
+
? {
|
|
309
|
+
beforeAuth: (a) => a.get(new URL(consent.redirectUri).pathname, consentCallback(consent, { requireCallerIdentity: callbackIdentity(process.env) === 'required', log, warn })),
|
|
310
|
+
}
|
|
311
|
+
: {}),
|
|
312
|
+
warn,
|
|
313
|
+
});
|
|
314
|
+
closeHttp = app.closeSessions;
|
|
315
|
+
// The secret is not needed past this point.
|
|
316
|
+
secret = undefined;
|
|
317
|
+
await new Promise((resolve) => {
|
|
318
|
+
app.listen(port, host, () => resolve());
|
|
319
|
+
});
|
|
320
|
+
log(`✓ Serving ${syndicates.length} syndicate tool(s) at http://${host.includes(':') ? `[${host}]` : host}:${port}/mcp: ${toolList}`);
|
|
321
|
+
log(` auth ${process.env.MCP_SERVER_SECRET?.trim() ? 'bearer MCP_SERVER_SECRET' : 'none (loopback only)'}`);
|
|
322
|
+
log(` storage ${pg ? 'postgres' : 'in-memory (lost on restart)'}`);
|
|
323
|
+
log(` oauth ${oauth.summary}`);
|
|
324
|
+
}
|
|
325
|
+
// Run-as-main guard on the realpath of argv[1] (the bin is a symlink; see a2a_server.ts).
|
|
326
|
+
const invokedAsMain = (() => {
|
|
327
|
+
if (!process.argv[1])
|
|
328
|
+
return false;
|
|
329
|
+
try {
|
|
330
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return import.meta.url === `file://${process.argv[1]}`;
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
if (invokedAsMain) {
|
|
337
|
+
// Before anything else loads: stdout is the stdio protocol channel.
|
|
338
|
+
console.log = console.error;
|
|
339
|
+
console.info = console.error;
|
|
340
|
+
console.debug = console.error;
|
|
341
|
+
startMcpServer().catch((error) => {
|
|
342
|
+
console.error(`[MCP] ✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/setup.ts — the `melchizedek-setup` bin and `npm run setup`.
|
|
4
|
+
*
|
|
5
|
+
* The onboarding command: a menu of authentication levels (no key, one
|
|
6
|
+
* provider, several, a gateway, a cloud platform, per-caller keys, caller
|
|
7
|
+
* tokens, OAuth tool grants, and an honest entry for subscription sign-ins),
|
|
8
|
+
* each with a startup guide: the variables to set (names and shapes only),
|
|
9
|
+
* the doctor command that confirms it, the shipped files that run there and
|
|
10
|
+
* the first commands to try. The levels and guides live in lib/onboarding.ts;
|
|
11
|
+
* ONBOARDING.md is generated from the same function.
|
|
12
|
+
*
|
|
13
|
+
* npx melchizedek-setup # the menu (interactive terminal)
|
|
14
|
+
* npx melchizedek-setup --auto # detect from the environment, names only
|
|
15
|
+
* npx melchizedek-setup --level gateway # one guide, by id or number
|
|
16
|
+
* npx melchizedek-setup --level 2 --write-env # also create .env from .env.example
|
|
17
|
+
* npx melchizedek-setup --list
|
|
18
|
+
* npx melchizedek-setup --markdown # ONBOARDING.md, regenerated
|
|
19
|
+
* npx melchizedek-setup --chatgpt-signin # Sign in with ChatGPT, local only (ADR 0126)
|
|
20
|
+
* npx melchizedek-setup --chatgpt-signout | --chatgpt-status
|
|
21
|
+
*
|
|
22
|
+
* Never prints a value. `--write-env` never overwrites `.env`, leaves every
|
|
23
|
+
* name blank, and refuses unless `git check-ignore` confirms `.env` is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export declare function main(argv?: string[]): Promise<number>;
|