@jamestesting888/tum-mcp 0.5.0 → 0.6.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/CHANGELOG.md +29 -0
- package/NOTICE +2 -4
- package/README.md +162 -57
- package/docs/development.md +118 -92
- package/docs/setup.md +79 -15
- package/docs/study.md +325 -0
- package/docs/tum-mcp.md +115 -125
- package/npm/backend/manifest.json +3 -3
- package/npm/backend/requirements.txt +22 -18
- package/npm/backend/tum_mcp-0.6.0-py3-none-any.whl +0 -0
- package/npm/runtime.mjs +210 -62
- package/npm/tum-mcp.mjs +20 -8
- package/package.json +49 -10
- package/npm/backend/tum_moodle_integration-0.5.0-py3-none-any.whl +0 -0
package/npm/runtime.mjs
CHANGED
|
@@ -1,96 +1,244 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { chmod, mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { chmod, mkdir, mkdtemp, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir, hostname } from 'node:os';
|
|
6
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { download } from './download.mjs';
|
|
9
9
|
|
|
10
10
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
11
|
const pins = JSON.parse(readFileSync(join(packageRoot, 'npm/uv-artifacts.json'), 'utf8'));
|
|
12
|
+
const packageVersion = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
12
13
|
|
|
13
14
|
export function dataRoot() {
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
let path = process.env.TUM_MCP_DATA_DIR;
|
|
16
|
+
if (!path) {
|
|
17
|
+
if (process.platform === 'darwin') path = join(homedir(), 'Library/Application Support/TUMMCP');
|
|
18
|
+
else if (process.platform === 'win32') path = join(process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local'), 'TUMMCP');
|
|
19
|
+
else path = join(process.env.XDG_DATA_HOME || join(homedir(), '.local/share'), 'TUMMCP');
|
|
20
|
+
}
|
|
21
|
+
path = resolve(path.replace(/^~(?=$|[/\\])/, homedir()));
|
|
22
|
+
const tail = [];
|
|
23
|
+
while (!existsSync(path)) { tail.unshift(basename(path)); path = dirname(path); }
|
|
24
|
+
return join(realpathSync(path), ...tail);
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
function manifest() {
|
|
28
|
+
const value = JSON.parse(readFileSync(join(packageRoot, 'npm/backend/manifest.json'), 'utf8'));
|
|
29
|
+
if (!/^[a-zA-Z0-9_.-]+\.whl$/.test(value.wheel) ||
|
|
30
|
+
!/^[a-f0-9]{64}$/.test(value.wheelSha256) || !/^[a-f0-9]{64}$/.test(value.requirementsSha256)) {
|
|
31
|
+
throw new Error('Invalid backend manifest; reinstall the npm package.');
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
export function runtimeIdentity(value = manifest()) {
|
|
37
|
+
return createHash('sha256').update(JSON.stringify({ schema: 2, wheel: value.wheelSha256,
|
|
38
|
+
requirements: value.requirementsSha256, python: pins.python })).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function controlDirectory(identity) { return join(dataRoot(), 'runtimes', identity); }
|
|
42
|
+
function pythonPath(runtime) { return join(runtime, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'); }
|
|
43
|
+
|
|
28
44
|
export function currentPython() {
|
|
29
45
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
const runtime = join(
|
|
35
|
-
const expected =
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
46
|
+
const identity = runtimeIdentity();
|
|
47
|
+
const control = controlDirectory(identity);
|
|
48
|
+
const selected = JSON.parse(readFileSync(join(control, 'current.json'), 'utf8'));
|
|
49
|
+
if (selected.schema !== 2 || selected.identity !== identity || !/^env-[a-zA-Z0-9]+$/.test(selected.generation)) return null;
|
|
50
|
+
const runtime = join(control, selected.generation);
|
|
51
|
+
const expected = pythonPath(runtime);
|
|
52
|
+
if (selected.python !== expected || !existsSync(expected)) return null;
|
|
53
|
+
if ([join(dataRoot(), 'runtimes'), control, runtime, join(runtime, '.tum-mcp-ready')].some(path => lstatSync(path).isSymbolicLink())) return null;
|
|
54
|
+
if (readFileSync(join(runtime, '.tum-mcp-ready'), 'utf8') !== identity) return null;
|
|
38
55
|
return expected;
|
|
39
56
|
} catch { return null; }
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
function run(executable, args, { quiet = false, timeout = 600000 } = {}) {
|
|
60
|
+
const env = { ...process.env }; delete env.VIRTUAL_ENV; delete env.PYTHONPATH; delete env.PYTHONHOME;
|
|
61
|
+
const result = spawnSync(executable, args, { env, stdio: quiet ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'inherit', 'inherit'], shell: false, timeout });
|
|
62
|
+
if (result.error) throw new Error(`Managed runtime subprocess could not start (${result.error.code || 'unknown'}); check permissions and retry setup.`);
|
|
63
|
+
if (result.status !== 0) throw new Error('Managed runtime setup failed; the previous runtime was retained.');
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function healthy(python) {
|
|
68
|
+
try {
|
|
69
|
+
const result = run(python, ['-I', '-c', 'from tum_mcp.server import server; from tum_mcp import __version__; print(__version__)'], { quiet: true, timeout: 30000 });
|
|
70
|
+
return result.stdout.toString().trim() === packageVersion;
|
|
71
|
+
} catch { return false; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function atomicWrite(path, data) {
|
|
75
|
+
const temporary = `${path}.${randomUUID()}.pending`;
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(temporary, data, { mode: 0o600, flag: 'wx' });
|
|
78
|
+
await rename(temporary, path);
|
|
79
|
+
} finally { await rm(temporary, { force: true }); }
|
|
80
|
+
}
|
|
81
|
+
|
|
42
82
|
function matchingUv(executable) {
|
|
43
83
|
const found = spawnSync(executable, ['--version'], { encoding: 'utf8', shell: false, timeout: 10000 });
|
|
44
84
|
return found.status === 0 && found.stdout.trim().split(' ')[1] === pins.version;
|
|
45
85
|
}
|
|
46
86
|
|
|
87
|
+
async function uvExecutable() {
|
|
88
|
+
const cached = join(dataRoot(), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
89
|
+
if (matchingUv('uv')) return 'uv';
|
|
90
|
+
if (matchingUv(cached)) {
|
|
91
|
+
await atomicWrite(join(dataRoot(), 'uv-owner.json'), JSON.stringify({ sha256: createHash('sha256').update(readFileSync(cached)).digest('hex') }));
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
const systems = { darwin: 'apple-darwin', linux: 'unknown-linux-gnu', win32: 'pc-windows-msvc' };
|
|
95
|
+
const arches = { arm64: 'aarch64', x64: 'x86_64' };
|
|
96
|
+
const target = `${arches[process.arch]}-${systems[process.platform]}`;
|
|
97
|
+
const artifact = pins.artifacts[target];
|
|
98
|
+
if (!artifact) throw new Error('Unsupported platform/architecture; use a documented manual installation.');
|
|
99
|
+
const temporary = await mkdtemp(join(dataRoot(), 'bootstrap-'));
|
|
100
|
+
try {
|
|
101
|
+
const archive = join(temporary, process.platform === 'win32' ? 'uv.zip' : 'uv.tar.gz');
|
|
102
|
+
await download(artifact.url, archive, artifact.sha256);
|
|
103
|
+
run('tar', ['-xf', archive, '-C', temporary]);
|
|
104
|
+
const extracted = process.platform === 'win32' ? join(temporary, 'uv.exe') : join(temporary, `uv-${target}`, 'uv');
|
|
105
|
+
await rename(extracted, cached);
|
|
106
|
+
if (process.platform !== 'win32') await chmod(cached, 0o700);
|
|
107
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
108
|
+
await atomicWrite(join(dataRoot(), 'uv-owner.json'), JSON.stringify({ sha256: createHash('sha256').update(readFileSync(cached)).digest('hex') }));
|
|
109
|
+
return cached;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function acquireSetupLock() {
|
|
113
|
+
const lock = join(dataRoot(), 'setup.lock');
|
|
114
|
+
const owner = { pid: process.pid, host: hostname(), nonce: randomUUID() };
|
|
115
|
+
const guard = join(dataRoot(), 'setup.claim');
|
|
116
|
+
try { await mkdir(guard, { mode: 0o700 }); }
|
|
117
|
+
catch { throw new Error('Another setup is claiming the lock. Retry; an interrupted setup.claim needs local review.'); }
|
|
118
|
+
try {
|
|
119
|
+
try { await mkdir(lock, { mode: 0o700 }); }
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code !== 'EEXIST') throw error;
|
|
122
|
+
let previous;
|
|
123
|
+
try { previous = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')); }
|
|
124
|
+
catch { throw new Error('Setup lock has no readable owner. Confirm no setup is running before removing setup.lock.'); }
|
|
125
|
+
if (!Number.isSafeInteger(previous.pid) || previous.pid <= 0 || previous.host !== hostname()) {
|
|
126
|
+
throw new Error('Setup lock belongs to an unknown owner; preserve it for local review.');
|
|
127
|
+
}
|
|
128
|
+
let alive = true;
|
|
129
|
+
try { process.kill(previous.pid, 0); }
|
|
130
|
+
catch (failure) { if (failure.code === 'ESRCH') alive = false; }
|
|
131
|
+
if (alive) throw new Error('Another setup is running; retry when it finishes.');
|
|
132
|
+
const abandoned = `${lock}.${randomUUID()}.abandoned`;
|
|
133
|
+
await rename(lock, abandoned);
|
|
134
|
+
await rm(abandoned, { recursive: true, force: true });
|
|
135
|
+
try { await mkdir(lock, { mode: 0o700 }); }
|
|
136
|
+
catch { throw new Error('Another setup acquired the lock; retry when it finishes.'); }
|
|
137
|
+
}
|
|
138
|
+
await atomicWrite(join(lock, 'owner.json'), JSON.stringify(owner));
|
|
139
|
+
} finally { await rmdir(guard); }
|
|
140
|
+
return async () => {
|
|
141
|
+
const current = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8'));
|
|
142
|
+
if (current.nonce !== owner.nonce) throw new Error('Setup lock ownership changed; it was preserved.');
|
|
143
|
+
await rm(lock, { recursive: true, force: true });
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
47
147
|
export async function ensureRuntime() {
|
|
48
148
|
const backend = join(packageRoot, 'npm/backend');
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
for (const [name, expected] of [[manifest.wheel, manifest.wheelSha256], ['requirements.txt', manifest.requirementsSha256]]) {
|
|
149
|
+
const value = manifest();
|
|
150
|
+
for (const [name, expected] of [[value.wheel, value.wheelSha256], ['requirements.txt', value.requirementsSha256]]) {
|
|
52
151
|
if (createHash('sha256').update(readFileSync(join(backend, name))).digest('hex') !== expected) throw new Error('Backend integrity check failed.');
|
|
53
152
|
}
|
|
54
|
-
const identity =
|
|
55
|
-
const runtime = join(dataRoot(), 'runtimes', identity);
|
|
56
|
-
const python = join(runtime, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
|
|
153
|
+
const identity = runtimeIdentity(value);
|
|
57
154
|
await mkdir(dataRoot(), { recursive: true, mode: 0o700 });
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try { await mkdir(lock, { mode: 0o700 }); }
|
|
61
|
-
catch (error) { if (error.code === 'EEXIST') throw new Error('Another setup is running; retry when it finishes.'); throw error; }
|
|
155
|
+
const release = await acquireSetupLock();
|
|
156
|
+
let preparing;
|
|
62
157
|
try {
|
|
63
|
-
|
|
64
|
-
if (!
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const arches = { arm64: 'aarch64', x64: 'x86_64' };
|
|
70
|
-
const target = `${arches[process.arch]}-${systems[process.platform]}`;
|
|
71
|
-
const artifact = pins.artifacts[target];
|
|
72
|
-
if (!artifact) throw new Error('Unsupported platform/architecture; use a documented manual installation.');
|
|
73
|
-
const temporary = await mkdtemp(join(dataRoot(), 'bootstrap-'));
|
|
74
|
-
try {
|
|
75
|
-
const archive = join(temporary, process.platform === 'win32' ? 'uv.zip' : 'uv.tar.gz');
|
|
76
|
-
await download(artifact.url, archive, artifact.sha256);
|
|
77
|
-
run('tar', ['-xf', archive, '-C', temporary]);
|
|
78
|
-
const extracted = process.platform === 'win32' ? join(temporary, 'uv.exe') : join(temporary, `uv-${target}`, 'uv');
|
|
79
|
-
uv = join(dataRoot(), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
80
|
-
await rename(extracted, uv); if (process.platform !== 'win32') await chmod(uv, 0o700);
|
|
81
|
-
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
158
|
+
let python = currentPython();
|
|
159
|
+
if (!python || !healthy(python)) {
|
|
160
|
+
const uv = await uvExecutable();
|
|
161
|
+
const control = controlDirectory(identity);
|
|
162
|
+
for (const path of [join(dataRoot(), 'runtimes'), control]) {
|
|
163
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) throw new Error('Symbolic runtime directory preserved; review it locally.');
|
|
82
164
|
}
|
|
83
|
-
await mkdir(
|
|
84
|
-
|
|
85
|
-
|
|
165
|
+
await mkdir(control, { recursive: true, mode: 0o700 });
|
|
166
|
+
await atomicWrite(join(control, 'runtime-owner.json'), JSON.stringify({ kind: 'tum-mcp-runtime', schema: 2, identity }));
|
|
167
|
+
preparing = await mkdtemp(join(control, 'env-'));
|
|
168
|
+
python = pythonPath(preparing);
|
|
169
|
+
run(uv, ['venv', '--managed-python', '--python', pins.python, preparing]);
|
|
86
170
|
run(uv, ['pip', 'sync', '--python', python, '--require-hashes', '--only-binary', ':all:', join(backend, 'requirements.txt')]);
|
|
87
|
-
run(uv, ['pip', 'install', '--python', python, '--no-deps', '--no-index', join(backend,
|
|
171
|
+
run(uv, ['pip', 'install', '--python', python, '--no-deps', '--no-index', join(backend, value.wheel)]);
|
|
172
|
+
if (!healthy(python)) throw new Error('New runtime validation failed; the previous runtime was retained.');
|
|
173
|
+
await atomicWrite(join(preparing, '.tum-mcp-ready'), identity);
|
|
174
|
+
const selected = JSON.stringify({ schema: 2, identity, generation: basename(preparing), python });
|
|
175
|
+
await atomicWrite(join(control, 'current.json'), selected);
|
|
176
|
+
// Published generations are never removed: existing clients may still pin one.
|
|
177
|
+
preparing = undefined;
|
|
178
|
+
await atomicWrite(join(dataRoot(), 'runtime.json'), selected);
|
|
88
179
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await
|
|
93
|
-
await rename(join(dataRoot(), 'runtime.pending.json'), join(dataRoot(), 'runtime.json'));
|
|
180
|
+
const selected = readFileSync(join(controlDirectory(identity), 'current.json'), 'utf8');
|
|
181
|
+
const global = join(dataRoot(), 'runtime.json');
|
|
182
|
+
if (!existsSync(global) || readFileSync(global, 'utf8') !== selected) await atomicWrite(global, selected);
|
|
183
|
+
await rm(join(dataRoot(), 'runtime-removal.pending'), { force: true });
|
|
94
184
|
return python;
|
|
95
|
-
} finally {
|
|
185
|
+
} finally {
|
|
186
|
+
try { if (preparing) await rm(preparing, { recursive: true, force: true }); }
|
|
187
|
+
finally { await release(); }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
export async function removeRuntimeFiles() {
|
|
193
|
+
const root = dataRoot();
|
|
194
|
+
const directory = join(root, 'runtimes');
|
|
195
|
+
if (!existsSync(join(root, 'runtime-removal.pending'))) throw new Error('Runtime removal must be requested with uninstall --all --purge-runtime.');
|
|
196
|
+
const owned = [];
|
|
197
|
+
if (existsSync(directory)) {
|
|
198
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error('Symbolic runtime directory preserved for review.');
|
|
199
|
+
for (const name of readdirSync(directory)) {
|
|
200
|
+
const runtime = join(directory, name);
|
|
201
|
+
if (!/^[a-f0-9]{64}$/.test(name) || lstatSync(runtime).isSymbolicLink()) throw new Error('Unrecognized runtime files were preserved for review.');
|
|
202
|
+
let verified = false;
|
|
203
|
+
const owner = join(runtime, 'runtime-owner.json');
|
|
204
|
+
if (existsSync(owner) && !lstatSync(owner).isSymbolicLink()) {
|
|
205
|
+
const record = JSON.parse(readFileSync(owner, 'utf8'));
|
|
206
|
+
verified = record.kind === 'tum-mcp-runtime' && record.schema === 2 && record.identity === name;
|
|
207
|
+
}
|
|
208
|
+
const legacy = join(runtime, '.tum-mcp-ready');
|
|
209
|
+
if (!verified && existsSync(legacy) && !lstatSync(legacy).isSymbolicLink()) verified = readFileSync(legacy, 'utf8') === name;
|
|
210
|
+
if (!verified) throw new Error('Runtime ownership could not be verified; files were preserved.');
|
|
211
|
+
owned.push(runtime);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const pointer = join(root, 'runtime.json');
|
|
215
|
+
let removePointer = false;
|
|
216
|
+
if (existsSync(pointer)) {
|
|
217
|
+
let current;
|
|
218
|
+
try { current = JSON.parse(readFileSync(pointer, 'utf8')); }
|
|
219
|
+
catch { throw new Error('Runtime selection metadata is unreadable; runtimes were preserved. Run setup to repair it.'); }
|
|
220
|
+
removePointer = /^[a-f0-9]{64}$/.test(current.identity) && owned.includes(join(directory, current.identity));
|
|
221
|
+
}
|
|
222
|
+
// All candidates are checked before deletion; never remove the campus data root.
|
|
223
|
+
for (const runtime of owned) await rm(runtime, { recursive: true });
|
|
224
|
+
if (existsSync(directory) && readdirSync(directory).length === 0) await rmdir(directory);
|
|
225
|
+
if (removePointer) await rm(pointer);
|
|
226
|
+
const uv = join(root, process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
227
|
+
const uvOwner = join(root, 'uv-owner.json');
|
|
228
|
+
if (existsSync(uvOwner) && existsSync(uv)) {
|
|
229
|
+
const expected = JSON.parse(readFileSync(uvOwner, 'utf8')).sha256;
|
|
230
|
+
if (!lstatSync(uv).isSymbolicLink() && createHash('sha256').update(readFileSync(uv)).digest('hex') === expected) {
|
|
231
|
+
await rm(uv); await rm(uvOwner);
|
|
232
|
+
} else console.error('Modified bootstrap cache was preserved for local review.');
|
|
233
|
+
}
|
|
234
|
+
const leases = join(root, 'runtime-leases');
|
|
235
|
+
if (existsSync(leases) && !lstatSync(leases).isSymbolicLink()) {
|
|
236
|
+
for (const name of readdirSync(leases)) {
|
|
237
|
+
const lock = join(leases, name);
|
|
238
|
+
if (/^[a-f0-9]{32}\.lock$/.test(name) && !lstatSync(lock).isSymbolicLink()) await rm(lock);
|
|
239
|
+
}
|
|
240
|
+
if (readdirSync(leases).length === 0) await rmdir(leases);
|
|
241
|
+
}
|
|
242
|
+
await rm(join(root, 'runtime-removal.pending'), { force: true });
|
|
243
|
+
console.log(`Removed ${owned.length} managed runtime(s). Saved user data was retained.`);
|
|
96
244
|
}
|
package/npm/tum-mcp.mjs
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { acquireSetupLock, currentPython, ensureRuntime, removeRuntimeFiles } from './runtime.mjs';
|
|
4
5
|
|
|
5
6
|
const args = process.argv.slice(2);
|
|
7
|
+
const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
6
8
|
try {
|
|
7
9
|
if (args.length === 0 || (args.length === 1 && ['--help', '-h'].includes(args[0]))) {
|
|
8
|
-
console.log(
|
|
10
|
+
console.log(`TUM MCP ${version}\nCommands: setup [--client codex|claude|cursor|antigravity|generic], connect, scope, discover, status, doctor [--json] [--live], serve, skills, study, logout, uninstall, update\nRun connect without arguments to select services and an installed Chromium browser. Run setup explicitly to provision the managed Python backend.`);
|
|
9
11
|
} else if (['--version', 'version'].includes(args[0])) {
|
|
10
|
-
console.log(
|
|
12
|
+
console.log(version);
|
|
13
|
+
} else if (['setup', 'update'].includes(args[0]) && args.length === 2 && ['--help', '-h'].includes(args[1])) {
|
|
14
|
+
console.log(args[0] === 'update'
|
|
15
|
+
? 'Usage: tum-mcp update\nRefresh previously configured clients for the installed npm version.\nDownload a newer version first: npm install --global @jamestesting888/tum-mcp@latest'
|
|
16
|
+
: 'Usage: tum-mcp setup [--client codex|claude|cursor|antigravity|generic] [--host local|windows]\nProvision the managed runtime and register explicitly selected clients.');
|
|
11
17
|
} else {
|
|
18
|
+
if (args[0] === 'update' && args.length !== 1) throw new Error('Usage: tum-mcp update (no arguments). Use --help for the update workflow.');
|
|
12
19
|
const setup = ['setup', 'update'].includes(args[0]);
|
|
13
20
|
const python = setup ? await ensureRuntime() : currentPython();
|
|
14
21
|
if (!python) throw new Error('SETUP_REQUIRED: Run tum-mcp setup first.');
|
|
15
|
-
const forwarded = args
|
|
16
|
-
const env = { ...process.env }; delete env.PYTHONPATH; delete env.PYTHONHOME;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const forwarded = args;
|
|
23
|
+
const env = { ...process.env, TUM_MCP_NPM_LAUNCHER: '1' }; delete env.PYTHONPATH; delete env.PYTHONHOME;
|
|
24
|
+
const purge = args[0] === 'uninstall' && args.includes('--purge-runtime');
|
|
25
|
+
const release = purge ? await acquireSetupLock() : null;
|
|
26
|
+
try {
|
|
27
|
+
const child = spawnSync(python, ['-I', '-m', 'tum_mcp', ...forwarded], { env, stdio: 'inherit', shell: false });
|
|
28
|
+
if (child.error) throw new Error('Managed backend could not start; run setup again.');
|
|
29
|
+
process.exitCode = child.status ?? (child.signal === 'SIGINT' ? 130 : 1);
|
|
30
|
+
if (purge && child.status === 0) await removeRuntimeFiles();
|
|
31
|
+
} finally { if (release) await release(); }
|
|
20
32
|
}
|
|
21
33
|
} catch (error) { console.error(error.message); process.exitCode = 2; }
|
package/package.json
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamestesting888/tum-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Read-only TUM Moodle, Artemis and Zulip integration with source-backed study workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"tum-mcp": "npm/tum-mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.14.0"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"npm/tum-mcp.mjs",
|
|
15
|
+
"npm/runtime.mjs",
|
|
16
|
+
"npm/download.mjs",
|
|
17
|
+
"npm/uv-artifacts.json",
|
|
18
|
+
"npm/backend/*.whl",
|
|
19
|
+
"npm/backend/requirements.txt",
|
|
20
|
+
"npm/backend/manifest.json",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"NOTICE",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"docs/setup.md",
|
|
26
|
+
"docs/development.md",
|
|
27
|
+
"docs/tum-mcp.md",
|
|
28
|
+
"docs/study.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"prepack": "node npm/build.mjs",
|
|
32
|
+
"test": "node --test npm/test/*.test.mjs",
|
|
33
|
+
"test:consumer": "node npm/smoke-consumer.mjs",
|
|
34
|
+
"test:upgrade": "node npm/upgrade-consumer.mjs"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/jamestesting0/tum-mcp.git"
|
|
42
|
+
},
|
|
13
43
|
"homepage": "https://github.com/jamestesting0/tum-mcp#readme",
|
|
14
|
-
"bugs": {
|
|
15
|
-
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/jamestesting0/tum-mcp/issues"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"mcp",
|
|
49
|
+
"tum",
|
|
50
|
+
"moodle",
|
|
51
|
+
"artemis",
|
|
52
|
+
"zulip",
|
|
53
|
+
"codex"
|
|
54
|
+
]
|
|
16
55
|
}
|
|
Binary file
|