@jamestesting888/tum-mcp 0.0.0-stage → 0.4.1

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.
@@ -0,0 +1,38 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import https from 'node:https';
4
+
5
+ const hosts = new Set(['github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com', 'releases.astral.sh']);
6
+
7
+ export async function download(url, destination, expected, redirects = 0) {
8
+ const target = new URL(url);
9
+ if (target.protocol !== 'https:' || !hosts.has(target.hostname) || redirects > 5) throw new Error('Untrusted runtime download origin.');
10
+ await new Promise((resolve, reject) => {
11
+ const request = https.get(target, { timeout: 30000, headers: { 'User-Agent': 'tum-mcp-bootstrap' } }, response => {
12
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
13
+ response.resume();
14
+ if (!response.headers.location) return reject(new Error('Runtime redirect has no location.'));
15
+ download(new URL(response.headers.location, target).href, destination, expected, redirects + 1).then(resolve, reject);
16
+ return;
17
+ }
18
+ if (response.statusCode !== 200) { response.resume(); reject(new Error(`Runtime download returned HTTP ${response.statusCode}.`)); return; }
19
+ let size = 0;
20
+ const chunks = [];
21
+ response.on('data', chunk => {
22
+ size += chunk.length;
23
+ if (size > 64 * 1024 * 1024) response.destroy(new Error('Runtime download exceeds the size budget.'));
24
+ else chunks.push(chunk);
25
+ });
26
+ response.on('error', reject);
27
+ response.on('end', async () => {
28
+ try {
29
+ const bytes = Buffer.concat(chunks);
30
+ if (createHash('sha256').update(bytes).digest('hex') !== expected) throw new Error('Runtime checksum mismatch.');
31
+ await writeFile(destination, bytes, { mode: 0o600 }); resolve();
32
+ } catch (error) { reject(error); }
33
+ });
34
+ });
35
+ request.on('timeout', () => request.destroy(new Error('Runtime download timed out.')));
36
+ request.on('error', reject);
37
+ });
38
+ }
@@ -0,0 +1,96 @@
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';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { download } from './download.mjs';
9
+
10
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const pins = JSON.parse(readFileSync(join(packageRoot, 'npm/uv-artifacts.json'), 'utf8'));
12
+
13
+ export function dataRoot() {
14
+ if (process.env.TUM_MCP_DATA_DIR) return process.env.TUM_MCP_DATA_DIR;
15
+ if (process.platform === 'darwin') return join(homedir(), 'Library/Application Support/TUMMCP');
16
+ if (process.platform === 'win32') return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local'), 'TUMMCP');
17
+ return join(process.env.XDG_DATA_HOME || join(homedir(), '.local/share'), 'TUMMCP');
18
+ }
19
+
20
+ function run(executable, args) {
21
+ const env = { ...process.env };
22
+ delete env.VIRTUAL_ENV; delete env.PYTHONPATH; delete env.PYTHONHOME;
23
+ const result = spawnSync(executable, args, { env, stdio: ['ignore', 'inherit', 'inherit'], shell: false, timeout: 600000 });
24
+ if (result.error) throw new Error(`Managed runtime subprocess could not start (${result.error.code || 'unknown'}); check executable permissions and mount options.`);
25
+ if (result.status !== 0) throw new Error('Managed runtime setup failed; inspect the diagnostics above.');
26
+ }
27
+
28
+ export function currentPython() {
29
+ try {
30
+ const current = JSON.parse(readFileSync(join(dataRoot(), 'runtime.json'), 'utf8'));
31
+ if (!/^[a-f0-9]{64}$/.test(current.identity)) return null;
32
+ const manifest = JSON.parse(readFileSync(join(packageRoot, 'npm/backend/manifest.json'), 'utf8'));
33
+ if (current.identity !== manifest.wheelSha256) return null;
34
+ const runtime = join(dataRoot(), 'runtimes', current.identity);
35
+ const expected = join(runtime, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
36
+ if (current.python !== expected || !existsSync(expected)) return null;
37
+ if (readFileSync(join(runtime, '.tum-mcp-ready'), 'utf8') !== current.identity) return null;
38
+ return expected;
39
+ } catch { return null; }
40
+ }
41
+
42
+ function matchingUv(executable) {
43
+ const found = spawnSync(executable, ['--version'], { encoding: 'utf8', shell: false, timeout: 10000 });
44
+ return found.status === 0 && found.stdout.trim().split(' ')[1] === pins.version;
45
+ }
46
+
47
+ export async function ensureRuntime() {
48
+ const backend = join(packageRoot, 'npm/backend');
49
+ const manifest = JSON.parse(readFileSync(join(backend, 'manifest.json'), 'utf8'));
50
+ if (!/^[a-zA-Z0-9_.-]+\.whl$/.test(manifest.wheel)) throw new Error('Invalid backend manifest.');
51
+ for (const [name, expected] of [[manifest.wheel, manifest.wheelSha256], ['requirements.txt', manifest.requirementsSha256]]) {
52
+ if (createHash('sha256').update(readFileSync(join(backend, name))).digest('hex') !== expected) throw new Error('Backend integrity check failed.');
53
+ }
54
+ const identity = manifest.wheelSha256;
55
+ const runtime = join(dataRoot(), 'runtimes', identity);
56
+ const python = join(runtime, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
57
+ await mkdir(dataRoot(), { recursive: true, mode: 0o700 });
58
+ // A per-runtime directory lock prevents concurrent setup from corrupting a venv.
59
+ const lock = join(dataRoot(), 'setup.lock');
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; }
62
+ try {
63
+ const ready = join(runtime, '.tum-mcp-ready');
64
+ if (!existsSync(python) || !existsSync(ready) || readFileSync(ready, 'utf8') !== identity) {
65
+ const cachedUv = join(dataRoot(), process.platform === 'win32' ? 'uv.exe' : 'uv');
66
+ let uv = matchingUv('uv') ? 'uv' : matchingUv(cachedUv) ? cachedUv : null;
67
+ if (!uv) {
68
+ const systems = { darwin: 'apple-darwin', linux: 'unknown-linux-gnu', win32: 'pc-windows-msvc' };
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 }); }
82
+ }
83
+ await mkdir(dirname(runtime), { recursive: true, mode: 0o700 });
84
+ if (existsSync(runtime)) await rm(runtime, { recursive: true, force: true });
85
+ run(uv, ['venv', '--managed-python', '--python', pins.python, runtime]);
86
+ 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, manifest.wheel)]);
88
+ }
89
+ try { run(python, ['-I', '-m', 'tum_mcp', 'version']); }
90
+ catch (error) { await rm(ready, { force: true }); throw error; }
91
+ await writeFile(ready, identity, { mode: 0o600 });
92
+ await writeFile(join(dataRoot(), 'runtime.pending.json'), JSON.stringify({ identity, python }), { mode: 0o600 });
93
+ await rename(join(dataRoot(), 'runtime.pending.json'), join(dataRoot(), 'runtime.json'));
94
+ return python;
95
+ } finally { await rm(lock, { recursive: true, force: true }); }
96
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { currentPython, ensureRuntime } from './runtime.mjs';
4
+
5
+ const args = process.argv.slice(2);
6
+ try {
7
+ if (args.length === 0 || (args.length === 1 && ['--help', '-h'].includes(args[0]))) {
8
+ console.log('TUM MCP 0.4.1\nCommands: setup [--client codex|claude|generic], connect, scope, discover, status, doctor, serve, skills, workspace, actions, logout, uninstall, update\nRun setup explicitly to provision the managed Python backend.');
9
+ } else if (['--version', 'version'].includes(args[0])) {
10
+ console.log('0.4.1');
11
+ } else {
12
+ const setup = ['setup', 'update'].includes(args[0]);
13
+ const python = setup ? await ensureRuntime() : currentPython();
14
+ if (!python) throw new Error('SETUP_REQUIRED: Run tum-mcp setup first.');
15
+ const forwarded = args[0] === 'update' ? ['status'] : args;
16
+ const env = { ...process.env }; delete env.PYTHONPATH; delete env.PYTHONHOME;
17
+ const child = spawnSync(python, ['-I', '-m', 'tum_mcp', ...forwarded], { env, stdio: 'inherit', shell: false });
18
+ if (child.error) throw new Error('Managed backend could not start; run setup again.');
19
+ process.exitCode = child.status ?? 1;
20
+ }
21
+ } catch (error) { console.error(error.message); process.exitCode = 2; }
@@ -0,0 +1,30 @@
1
+ {
2
+ "version": "0.12.5",
3
+ "python": "3.13.15",
4
+ "artifacts": {
5
+ "aarch64-apple-darwin": {
6
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-aarch64-apple-darwin.tar.gz",
7
+ "sha256": "5bb0e5fe008a773c3dbcb97ff79cd89e1241464fe9d2f986d52ad8f1b037bd62"
8
+ },
9
+ "x86_64-apple-darwin": {
10
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-x86_64-apple-darwin.tar.gz",
11
+ "sha256": "b3b2137477cf96c9686ebfb71524614cec780c673fd73e59bce099aef02e70e8"
12
+ },
13
+ "x86_64-unknown-linux-gnu": {
14
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-x86_64-unknown-linux-gnu.tar.gz",
15
+ "sha256": "68a509da24b06b4223a1c0175fb5eb5bc79342b76cbeff0cfe51ac3f5b17b6b2"
16
+ },
17
+ "aarch64-unknown-linux-gnu": {
18
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-aarch64-unknown-linux-gnu.tar.gz",
19
+ "sha256": "9bf43b4d1a07665bf64d4c4e710930b382321a785e0eb10aac07f46471f86a31"
20
+ },
21
+ "x86_64-pc-windows-msvc": {
22
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-x86_64-pc-windows-msvc.zip",
23
+ "sha256": "4c4d49d8738847d9b71ba319e49a5688c93eac0fe6204b1df24e98528dddf39a"
24
+ },
25
+ "aarch64-pc-windows-msvc": {
26
+ "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-aarch64-pc-windows-msvc.zip",
27
+ "sha256": "724279317fee6e5fa8ad1908e4eba2bbe764ef1ece5b3f4597927b62b1fe562a"
28
+ }
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@jamestesting888/tum-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.1",
4
+ "description": "Local-first TUM Moodle, Artemis, and Zulip integration with campus workflow skills",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {"tum-mcp": "npm/tum-mcp.mjs"},
8
+ "engines": {"node": ">=22.14.0"},
9
+ "files": ["npm/tum-mcp.mjs", "npm/runtime.mjs", "npm/download.mjs", "npm/uv-artifacts.json", "npm/backend/*.whl", "npm/backend/requirements.txt", "npm/backend/manifest.json", "LICENSE", "NOTICE", "README.md", "docs/setup.md", "docs/development.md", "docs/tum-mcp.md", "docs/credential-storage-options.md"],
10
+ "scripts": {"prepack": "node npm/build.mjs", "test": "node --test npm/test/*.test.mjs", "test:consumer": "node npm/smoke-consumer.mjs"},
11
+ "publishConfig": {"access": "public"},
12
+ "repository": {"type": "git", "url": "git+https://github.com/jamestesting0/tum-moodle-integration.git"},
13
+ "homepage": "https://github.com/jamestesting0/tum-moodle-integration#readme",
14
+ "bugs": {"url": "https://github.com/jamestesting0/tum-moodle-integration/issues"},
15
+ "keywords": ["mcp", "tum", "moodle", "artemis", "zulip", "codex"]
16
+ }