champollion-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +133 -0
- package/README.md +245 -0
- package/bin/server.js +19 -0
- package/instructions.md +234 -0
- package/package.json +50 -0
- package/src/index.js +1106 -0
- package/src/tools/forge.js +140 -0
- package/src/tools/harness.js +727 -0
- package/src/tools/languages.js +329 -0
- package/src/tools/queue.js +313 -0
- package/src/tools/reliability.js +190 -0
- package/src/tools/results.js +346 -0
- package/src/tools/training.js +349 -0
- package/src/tools/translate.js +385 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forge_* — drive nmt-forge (the monorepo forge/ training suite) from an
|
|
3
|
+
* agent, one guarded step at a time.
|
|
4
|
+
*
|
|
5
|
+
* get_training_guardrails answers "what are the rules?"; these tools answer
|
|
6
|
+
* "where am I and what do I run next?" against a real workspace. Every tool
|
|
7
|
+
* shells out to the forge CLI with `--json` and relays the structured result;
|
|
8
|
+
* forge's refusals (what/why/fix) pass through verbatim, because a refusal is
|
|
9
|
+
* the product — it is the guard doing its job.
|
|
10
|
+
*
|
|
11
|
+
* The long-running GPU job (`nmt-forge run`) is deliberately NOT a tool: it
|
|
12
|
+
* would blow past any MCP client timeout. forge_status hands back the exact
|
|
13
|
+
* `run` command and the watcher guidance instead; forge_evaluate closes the
|
|
14
|
+
* loop after the run completes (decode+score+diagnose — CPU-cheap).
|
|
15
|
+
*
|
|
16
|
+
* Resolution: the forge package is invoked as `python3 -m nmt_forge.cli` with
|
|
17
|
+
* PYTHONPATH pointed at the forge/ directory (monorepo sibling of mcp-server,
|
|
18
|
+
* overridable via CHAMPOLLION_FORGE_DIR). An installed console script can be
|
|
19
|
+
* forced with NMT_FORGE_BIN. The Python interpreter is PYTHON_BIN or python3.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { resolve, dirname } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
// src/tools/forge.js → mcp-server root is two levels up
|
|
28
|
+
const MCP_ROOT = resolve(HERE, '../..');
|
|
29
|
+
|
|
30
|
+
/** Absolute path to the forge/ package directory. */
|
|
31
|
+
export function forgeDir() {
|
|
32
|
+
return process.env.CHAMPOLLION_FORGE_DIR || resolve(MCP_ROOT, '../forge');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build the argv + env to invoke a forge subcommand — PURE, unit-testable.
|
|
37
|
+
*
|
|
38
|
+
* The global `--workspace` flag precedes the subcommand (argparse layout).
|
|
39
|
+
* Callers pass already-validated positionals/flags in `subArgs`.
|
|
40
|
+
*
|
|
41
|
+
* @param {string[]} subArgs subcommand + its args, e.g. ['status', '--json']
|
|
42
|
+
* @param {{workspace?: string}} [opts]
|
|
43
|
+
* @returns {{cmd: string, args: string[], env: object, cwd: string}}
|
|
44
|
+
*/
|
|
45
|
+
export function buildForgeInvocation(subArgs, { workspace } = {}) {
|
|
46
|
+
const ws = workspace || process.env.CHAMPOLLION_FORGE_WORKSPACE || '.forge';
|
|
47
|
+
const globals = ['--workspace', ws];
|
|
48
|
+
const dir = forgeDir();
|
|
49
|
+
if (process.env.NMT_FORGE_BIN) {
|
|
50
|
+
return {
|
|
51
|
+
cmd: process.env.NMT_FORGE_BIN,
|
|
52
|
+
args: [...globals, ...subArgs],
|
|
53
|
+
env: { ...process.env },
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
cmd: process.env.PYTHON_BIN || 'python3',
|
|
59
|
+
args: ['-m', 'nmt_forge.cli', ...globals, ...subArgs],
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
PYTHONPATH: process.env.PYTHONPATH
|
|
63
|
+
? `${dir}:${process.env.PYTHONPATH}`
|
|
64
|
+
: dir,
|
|
65
|
+
},
|
|
66
|
+
cwd: process.cwd(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Spawn forge, capture output. No shell. stdin ignored (MCP stdio parent's
|
|
72
|
+
* stdin is the JSON-RPC stream — a child must never read it).
|
|
73
|
+
*
|
|
74
|
+
* @param {string[]} subArgs
|
|
75
|
+
* @param {{workspace?: string, timeout?: number}} [opts]
|
|
76
|
+
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
77
|
+
*/
|
|
78
|
+
export function runForge(subArgs, { workspace, timeout = 120_000 } = {}) {
|
|
79
|
+
const { cmd, args, env, cwd } = buildForgeInvocation(subArgs, { workspace });
|
|
80
|
+
return new Promise((resolvePromise, reject) => {
|
|
81
|
+
const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env, cwd, timeout });
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
proc.stdout.on('data', (d) => { stdout += d; });
|
|
85
|
+
proc.stderr.on('data', (d) => { stderr += d; });
|
|
86
|
+
proc.on('close', (code) => {
|
|
87
|
+
// The most common cold-start failure is python not finding the forge
|
|
88
|
+
// package at all — translate the raw traceback into the fix.
|
|
89
|
+
if ((code ?? 1) !== 0 && /No module named '?nmt_forge'?/.test(stderr)) {
|
|
90
|
+
stderr += '\n[forge] nmt_forge is not importable. forge is part of the '
|
|
91
|
+
+ 'Champollion monorepo (not on PyPI): clone '
|
|
92
|
+
+ 'https://github.com/gamedaysuits/Champollion and point '
|
|
93
|
+
+ 'CHAMPOLLION_FORGE_DIR at its forge/ directory. Scoring also needs '
|
|
94
|
+
+ 'the eval harness: pip install mt-eval';
|
|
95
|
+
}
|
|
96
|
+
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
97
|
+
});
|
|
98
|
+
proc.on('error', (err) => reject(new Error(
|
|
99
|
+
`could not launch forge (${cmd}): ${err.message}. Is forge/ present `
|
|
100
|
+
+ `(set CHAMPOLLION_FORGE_DIR) and python3 available?`)));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run a forge subcommand and shape the MCP result. Parses stdout as JSON when
|
|
106
|
+
* the subcommand emits JSON (most do with --json / _print); otherwise relays
|
|
107
|
+
* text. A nonzero exit is surfaced as isError with the guard's message.
|
|
108
|
+
*
|
|
109
|
+
* @param {string[]} subArgs
|
|
110
|
+
* @param {{workspace?: string, parseJson?: boolean, nextHint?: string}} [opts]
|
|
111
|
+
* @returns {Promise<{content: Array, isError: boolean}>}
|
|
112
|
+
*/
|
|
113
|
+
export async function forgeTool(subArgs, { workspace, parseJson = true, nextHint } = {}) {
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = await runForge(subArgs, { workspace });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
119
|
+
}
|
|
120
|
+
if (res.code !== 0) {
|
|
121
|
+
// forge writes what/why/fix to stderr on a ForgeError (exit 2)
|
|
122
|
+
const msg = (res.stderr || res.stdout || 'forge exited nonzero').trim();
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: `forge refused (exit ${res.code}):\n${msg}` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
let payload = res.stdout.trim();
|
|
129
|
+
if (parseJson) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(payload);
|
|
132
|
+
const out = { result: parsed };
|
|
133
|
+
if (nextHint) out.next = nextHint;
|
|
134
|
+
payload = JSON.stringify(out, null, 2);
|
|
135
|
+
} catch {
|
|
136
|
+
// not JSON (some commands print human text) — relay as-is
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { content: [{ type: 'text', text: payload }], isError: false };
|
|
140
|
+
}
|