@jamestesting888/tum-mcp 0.6.0 → 0.6.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.
- package/CHANGELOG.md +15 -2
- package/README.md +13 -15
- package/docs/development.md +6 -11
- package/docs/setup.md +27 -39
- package/docs/tum-mcp.md +32 -49
- package/npm/backend/manifest.json +2 -2
- package/npm/backend/{tum_mcp-0.6.0-py3-none-any.whl → tum_mcp-0.6.1-py3-none-any.whl} +0 -0
- package/npm/lock.mjs +43 -0
- package/npm/npm-command.mjs +96 -0
- package/npm/package-manager.mjs +109 -0
- package/npm/runtime.mjs +3 -35
- package/npm/tum-mcp.mjs +45 -20
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.6.1 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
- Make `tum-mcp update` download and validate the latest npm release before
|
|
6
|
+
replacing the invoked installation and refreshing configured clients.
|
|
7
|
+
Preserve newer installed versions; `--local` refreshes without contacting npm.
|
|
8
|
+
- Make `tum-mcp uninstall` remove managed client setup and the npm package in
|
|
9
|
+
one command. Keep saved data by default; stop package removal on conflicts.
|
|
10
|
+
Client-only removal retains npm, and `--keep-package` supports managed cleanup.
|
|
11
|
+
- Run npm through Node on all platforms, serialize package operations, and use
|
|
12
|
+
an external worker while Windows replaces package files.
|
|
13
|
+
- Test actual global installations against a private synthetic registry, including
|
|
14
|
+
failed downloads/preparation/replacement, customized files, and data preservation.
|
|
15
|
+
|
|
3
16
|
## 0.6.0 — 2026-10-25
|
|
4
17
|
|
|
5
18
|
- Merge course setup and updates into `sync-study`, with recovery for incomplete
|
|
@@ -20,8 +33,8 @@
|
|
|
20
33
|
Reuse isolated test fixtures,
|
|
21
34
|
retain native/platform coverage and expose slow tests and failures in CI.
|
|
22
35
|
- Preserve Windows CRLF study instructions through repeated synchronization.
|
|
23
|
-
- Group Artemis and Zulip implementation by provider, retaining read-only
|
|
24
|
-
tools
|
|
36
|
+
- Group Artemis and Zulip implementation by provider, retaining read-only
|
|
37
|
+
course tools. Fix OCR with aliased macOS temporary paths.
|
|
25
38
|
|
|
26
39
|
Upgrading does not rebuild personal study sites automatically. Ask your agent to
|
|
27
40
|
update the prepared website; customized renderer files are preserved for review.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Connect your coding agent to Moodle, Artemis and Zulip. Read course content and
|
|
4
4
|
build a private, source-backed study workspace. Unofficial; independent of TUM.
|
|
5
5
|
|
|
6
|
-
**Latest release: [0.6.
|
|
6
|
+
**Latest release: [0.6.1](https://www.npmjs.com/package/@jamestesting888/tum-mcp).**
|
|
7
7
|
[Release notes](CHANGELOG.md).
|
|
8
8
|
|
|
9
9
|
## Agent setup
|
|
@@ -36,13 +36,9 @@ Select services with **Space**, then **Enter**. Choose an installed browser and
|
|
|
36
36
|
complete sign-in there. Only selected tabs open; Zulip needs no API key.
|
|
37
37
|
Enable the [course and channel scopes](docs/tum-mcp.md#connections-and-scopes) you want to use.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| Claude Code | `--client claude` · MCP registration and skills |
|
|
43
|
-
| Cursor | `--client cursor` · MCP registration and all five skills; restart |
|
|
44
|
-
| Google Antigravity | `--client antigravity` · MCP registration and all five skills; restart |
|
|
45
|
-
| Other MCP clients | `--client generic` · configuration for manual installation |
|
|
39
|
+
Choose a client with `--client`: `codex`, `claude`, `cursor` or `antigravity`.
|
|
40
|
+
Setup configures MCP and installs all five skills; follow any activation or
|
|
41
|
+
restart prompts. Use `generic` for manual MCP setup.
|
|
46
42
|
|
|
47
43
|
For existing Cursor and Antigravity setups, run `tum-mcp update` to add the skills.
|
|
48
44
|
Updates and uninstall preserve customized files and report them for review.
|
|
@@ -51,7 +47,7 @@ Supports macOS, Windows, glibc Linux x86_64/ARM64 and WSL 3.x. In WSL, sign-in
|
|
|
51
47
|
opens a Windows browser. Add `--host windows` to setup for a Windows client that
|
|
52
48
|
uses the WSL backend. [Browser and WSL setup](docs/setup.md#wsl).
|
|
53
49
|
|
|
54
|
-
Setup supplies a private Python runtime.
|
|
50
|
+
Setup supplies a private Python runtime. Connections use encrypted local
|
|
55
51
|
files and need no native keyring, Git or Docker.
|
|
56
52
|
|
|
57
53
|
## Read and study
|
|
@@ -167,11 +163,13 @@ stay local. Study progress is saved in your browser.
|
|
|
167
163
|
## Update
|
|
168
164
|
|
|
169
165
|
```sh
|
|
170
|
-
npm install --global @jamestesting888/tum-mcp@latest
|
|
171
166
|
tum-mcp update
|
|
172
167
|
```
|
|
173
168
|
|
|
174
|
-
`update`
|
|
169
|
+
`update` downloads the latest release, validates its backend, installs it in the
|
|
170
|
+
same npm location and refreshes configured clients. It never downgrades a newer
|
|
171
|
+
installation. Use `tum-mcp update --local` to refresh the installed version without checking npm.
|
|
172
|
+
From 0.6.0 or earlier, upgrade the npm package once to obtain this command.
|
|
175
173
|
Sessions, scopes and downloads carry forward. User-edited entries and skills are
|
|
176
174
|
preserved and reported for review. Reload the client; refresh its cached plugin
|
|
177
175
|
if needed. Study sites are updated separately through the study workflows.
|
|
@@ -179,14 +177,14 @@ if needed. Study sites are updated separately through the study workflows.
|
|
|
179
177
|
## Remove
|
|
180
178
|
|
|
181
179
|
```sh
|
|
182
|
-
tum-mcp uninstall
|
|
183
|
-
npm uninstall --global @jamestesting888/tum-mcp
|
|
180
|
+
tum-mcp uninstall
|
|
184
181
|
```
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
The npm package, managed registrations and unchanged skills are removed. Settings, sessions,
|
|
187
184
|
downloads and study projects are preserved. After closing clients, add
|
|
188
185
|
`--purge-runtime` to remove generated runtimes or `--forget-credentials` to
|
|
189
|
-
remove saved local
|
|
186
|
+
remove saved local credentials. [Removal details](docs/setup.md#update-and-remove).
|
|
187
|
+
Use `--client NAME` to remove just one client, or `--keep-package` to retain npm.
|
|
190
188
|
|
|
191
189
|
[Guide](docs/tum-mcp.md) · [Setup](docs/setup.md) · [Study workflows](docs/study.md) · [Development](docs/development.md)
|
|
192
190
|
|
package/docs/development.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
The npm package is the distribution. Its wheel contains one Python package,
|
|
4
4
|
`src/tum_mcp/`, with peer `moodle/`, `artemis/` and `zulip/` provider packages.
|
|
5
|
-
The retired standalone Moodle plugin and its checkout installer are no longer
|
|
6
|
-
maintained.
|
|
7
5
|
Client adapters still generate the configuration each host needs.
|
|
8
6
|
|
|
9
7
|
## Checks
|
|
@@ -19,6 +17,7 @@ npm test
|
|
|
19
17
|
npm pack --ignore-scripts --pack-destination dist
|
|
20
18
|
npm run test:consumer
|
|
21
19
|
npm run test:upgrade
|
|
20
|
+
npm run test:package-lifecycle
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
Python 3.13 is the managed runtime. Install Tesseract for OCR tests. Default tests
|
|
@@ -57,8 +56,11 @@ CI runs macOS, Windows and Linux, plus both distribution engines on WSL 3.x.
|
|
|
57
56
|
Consumer checks install the actual npm artifact, discover tools, build a study
|
|
58
57
|
site and test available Chromium browsers with temporary profiles and synthetic
|
|
59
58
|
pages. WSL verifies Windows browser transport. No live account is used.
|
|
60
|
-
Upgrade checks cover public 0.
|
|
59
|
+
Upgrade checks cover public 0.6.0, client refresh, customized-file conflicts,
|
|
61
60
|
failed preparation, runtime repair and removal that preserves work/credentials.
|
|
61
|
+
The package-lifecycle check uses a local synthetic registry and real isolated
|
|
62
|
+
global npm prefixes to verify downloads, backend validation, replacement,
|
|
63
|
+
failure ordering and one-command removal. It does not publish a test release.
|
|
62
64
|
All five jobs must pass on the exact pushed commit before completion is claimed.
|
|
63
65
|
|
|
64
66
|
Live Moodle checks are opt-in: `TUM_MOODLE_LIVE=1 uv run pytest tests/live -q`.
|
|
@@ -97,14 +99,7 @@ tag and artifact integrity before pushing the release documentation.
|
|
|
97
99
|
|
|
98
100
|
Client setup installs managed copies of the repository's skills; those copies are
|
|
99
101
|
outputs, not another source tree. Use `skills customize` for a personal override.
|
|
100
|
-
The Codex adapter generates a small host plugin
|
|
101
|
-
load skills and MCP together; it is separate from the removed top-level Moodle
|
|
102
|
-
plugin distribution.
|
|
103
|
-
|
|
104
|
-
Keep Moodle's existing native-store service/account identity, session paths and
|
|
105
|
-
download locations stable during migrations. Environment variable names and the
|
|
106
|
-
`tum-moodle` CLI alias remain compatible; its implementation now resides inside
|
|
107
|
-
`tum_mcp.moodle`. No automatic native-store secret extraction occurs.
|
|
102
|
+
The Codex adapter generates a small host plugin to load skills and MCP together.
|
|
108
103
|
|
|
109
104
|
Extraction uses bounded subprocesses. POSIX enforces CPU/process-group limits;
|
|
110
105
|
Windows terminates the worker and observed children. Study imports and preview
|
package/docs/setup.md
CHANGED
|
@@ -35,11 +35,10 @@ Run **`tum-mcp connect`**, with no arguments, in an interactive terminal:
|
|
|
35
35
|
3. Complete TUM sign-in/MFA in the opened window. Only selected service tabs open.
|
|
36
36
|
4. Return to the terminal for the saved-connection result.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
No API key or
|
|
40
|
-
connection separate from your everyday browser. Ctrl+C closes it and preserves
|
|
38
|
+
All three services use the same sign-in and encrypted session storage flow.
|
|
39
|
+
No API key or native credential-store prompt is needed. A temporary browser
|
|
40
|
+
profile keeps the connection separate from your everyday browser. Ctrl+C closes it and preserves
|
|
41
41
|
connections that already completed. A dumb terminal uses numbered prompts.
|
|
42
|
-
Old service arguments and `--service`/`--auth` flags are no longer accepted.
|
|
43
42
|
|
|
44
43
|
Chrome, Edge, Brave, Chromium, Vivaldi and Opera are detected in their standard
|
|
45
44
|
locations. Selected preview channels and Opera GX are detected where installed.
|
|
@@ -113,19 +112,25 @@ browser discovery still requires functioning interop.
|
|
|
113
112
|
Sessions are encrypted with owner-restricted local key files. This avoids native
|
|
114
113
|
credential-store prompts, but anyone obtaining both key and ciphertext can
|
|
115
114
|
decrypt them. [Storage locations and recovery](tum-mcp.md#connections-and-scopes)
|
|
116
|
-
explain
|
|
115
|
+
explain where sessions are saved and how to back them up.
|
|
117
116
|
|
|
118
117
|
## Update and remove
|
|
119
118
|
|
|
120
119
|
```sh
|
|
121
|
-
npm install --global @jamestesting888/tum-mcp@latest
|
|
122
120
|
tum-mcp update
|
|
123
121
|
```
|
|
124
122
|
|
|
125
|
-
`update`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
From 0.6.1, `update` retrieves the latest npm release, validates its backend in a
|
|
124
|
+
temporary installation, replaces the invoked package and refreshes previously
|
|
125
|
+
configured clients. It preserves the installation prefix and global/local mode.
|
|
126
|
+
It does not connect services, change scopes or register additional clients.
|
|
127
|
+
It never downgrades a newer installation. `update --local` skips npm and repairs
|
|
128
|
+
or refreshes the installed version. Source/pinned builds use this local mode;
|
|
129
|
+
temporary npx and linked source installations are not replaced automatically.
|
|
130
|
+
Automatic replacement also stops if the package directory contains configured
|
|
131
|
+
sessions, downloads or course work; move that data outside the package first.
|
|
132
|
+
For 0.6.0 and earlier, first install the newer npm package once.
|
|
133
|
+
`setup --client NAME` explicitly registers one client and can also refresh that client's managed
|
|
129
134
|
files. WSL Windows registrations keep their distribution/user/data paths.
|
|
130
135
|
|
|
131
136
|
Ownership receipts track the entries and files installed by TUM MCP. Unchanged
|
|
@@ -138,57 +143,40 @@ not selected, and a damaged runtime is repaired into a fresh generation. A newer
|
|
|
138
143
|
managed client is not silently downgraded by an older package; intentional
|
|
139
144
|
rollback starts by unregistering that client, then registering the reviewed version.
|
|
140
145
|
|
|
141
|
-
0.4.1/0.5.0 installations without receipts are adopted only after matching their
|
|
142
|
-
retained runtime's canonical client output. The seven former workflow skills and the three website skills are replaced by
|
|
143
|
-
`read-content`, `sync-study` and `prepare-study`; `audio-notes` adds
|
|
144
|
-
spoken lessons derived from the prepared website. Personal overrides
|
|
145
|
-
under retired names remain in the config `skills/` directory; review and merge
|
|
146
|
-
retrieval guidance into `read-content` before exporting a replacement pack.
|
|
147
146
|
Reload MCP servers in Cursor/Antigravity, restart Claude as appropriate, and
|
|
148
147
|
update/reinstall TUM MCP from the personal source in Codex Plugins if its cached
|
|
149
148
|
copy still shows the older skills. Writing source files does not activate a host.
|
|
150
149
|
|
|
151
150
|
Remove a single client with `tum-mcp uninstall --client cursor` (replace the
|
|
152
151
|
client as needed; add `--host windows` from WSL for a Windows client). To remove
|
|
153
|
-
all managed registrations
|
|
152
|
+
the npm package and all managed registrations:
|
|
154
153
|
|
|
155
154
|
```sh
|
|
156
|
-
tum-mcp uninstall
|
|
157
|
-
npm uninstall --global @jamestesting888/tum-mcp
|
|
155
|
+
tum-mcp uninstall
|
|
158
156
|
```
|
|
159
157
|
|
|
160
158
|
Custom edits and user-added files are preserved. Codex's local source/catalog
|
|
161
159
|
entry can be removed, but its cached/active plugin must also be removed or
|
|
162
|
-
disabled through Codex Plugins.
|
|
160
|
+
disabled through Codex Plugins. Package removal runs only after successful
|
|
161
|
+
client cleanup; conflicts preserve npm so you can review and retry. `--all`
|
|
162
|
+
is accepted explicitly, and `--keep-package` removes client setup without npm.
|
|
163
|
+
No npm lifecycle hook performs cleanup or reads credentials.
|
|
163
164
|
|
|
164
165
|
For explicit cleanup, **close coding clients first**, then run:
|
|
165
166
|
|
|
166
167
|
```sh
|
|
167
|
-
tum-mcp uninstall --
|
|
168
|
-
npm uninstall --global @jamestesting888/tum-mcp
|
|
168
|
+
tum-mcp uninstall --purge-runtime --forget-credentials
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
`--purge-runtime` deletes verified managed runtime generations after the Python
|
|
172
172
|
helper exits, and removes unchanged managed bootstrap/browser-driver caches.
|
|
173
173
|
Active tracked backends or unrecognized runtime files stop automatic removal.
|
|
174
174
|
Paths overlapping configured assignment work, downloads or Moodle data are
|
|
175
|
-
protected. `--forget-credentials` removes local
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
work are retained by these commands. No command recursively deletes the campus
|
|
175
|
+
protected. `--forget-credentials` removes saved local credentials and session
|
|
176
|
+
keys; it does not revoke remote credentials. Settings, custom skills, course
|
|
177
|
+
cache, downloads and assignment work are retained by these commands.
|
|
178
|
+
No command recursively deletes the campus
|
|
180
179
|
data root. Course audio and optional shared speech models/runtimes also remain.
|
|
181
180
|
Remove a shared speech cache only when explicitly requested and no other course
|
|
182
181
|
needs it; its host paths are documented in the audio skill. If removal is
|
|
183
|
-
interrupted, run
|
|
184
|
-
|
|
185
|
-
## Existing Moodle-only installations
|
|
186
|
-
|
|
187
|
-
The older `tum-moodle` commands and sixteen `moodle_*` tools remain compatible.
|
|
188
|
-
The separate source/plugin installer retains its original native credential
|
|
189
|
-
store: macOS Keychain, Windows Credential Manager, or Linux Secret Service.
|
|
190
|
-
Its native-store identity, existing session paths and downloads remain intact.
|
|
191
|
-
The combined `tum-mcp serve` uses local-file storage and directs all login
|
|
192
|
-
requests to `tum-mcp connect`. Reconnect once through that selector to create the
|
|
193
|
-
new session; no automatic extraction of older native-store secrets occurs.
|
|
194
|
-
Disable the older plugin when activating TUM MCP to avoid duplicate tools.
|
|
182
|
+
interrupted, run `tum-mcp setup --client NAME` to recover before reuse.
|
package/docs/tum-mcp.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# TUM MCP guide
|
|
2
2
|
|
|
3
3
|
Package: `@jamestesting888/tum-mcp`, CLI: `tum-mcp`, backend: Python 3.13.
|
|
4
|
-
|
|
4
|
+
Development version **0.6.1 (unreleased)** adds npm package management to the
|
|
5
|
+
read-only course retrieval and five local study workflows in 0.6.0.
|
|
5
6
|
|
|
6
7
|
## Current capabilities
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
The sixteen `moodle_*` tools provide materials, announcements, forums and
|
|
10
|
+
explicitly scoped private-record reads. Artemis adds
|
|
10
11
|
available-course metadata, lectures and learning units, student PDF attachments,
|
|
11
12
|
released non-quiz statements and permitted exact-commit feedback. Zulip provides
|
|
12
13
|
scoped channel/topic search and cited discussion reads.
|
|
13
14
|
|
|
14
15
|
Website access is read-only. There are no public exercise workspace, test-run,
|
|
15
|
-
submission, start, deployment, rebuild or push workflows.
|
|
16
|
-
remote gate rejects every action even if an old verification flag is enabled.
|
|
16
|
+
submission, start, deployment, rebuild or push workflows.
|
|
17
17
|
Local downloads, source inventories, study-site builds and checkpoint storage
|
|
18
18
|
are local operations; they do not post, submit or mark campus content as read.
|
|
19
19
|
|
|
@@ -67,22 +67,19 @@ and Python 3.13.15; an existing matching uv, including the verified cached downl
|
|
|
67
67
|
is reused. Source checkouts and system
|
|
68
68
|
Python are unnecessary for consumers. Supported bootstrap targets are macOS
|
|
69
69
|
arm64/x64, Windows arm64/x64, and glibc Linux arm64/x64; hands-on verification is
|
|
70
|
-
recorded separately.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
and
|
|
77
|
-
the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`update` prepares the backend bundled in the invoked package, refreshes
|
|
81
|
-
previously configured clients and preserves older runtime generations.
|
|
70
|
+
recorded separately. Campus sessions use permission-restricted local keys.
|
|
71
|
+
CI checks clean installation, updates, removal, MCP discovery and synthetic
|
|
72
|
+
browser workflows on macOS, Windows, Linux and both WSL engines. See the
|
|
73
|
+
[development guide](development.md#checks) for the verification layers.
|
|
74
|
+
|
|
75
|
+
`update` downloads and validates the latest release, replaces the invoked npm
|
|
76
|
+
installation and refreshes previously configured clients. `update --local`
|
|
77
|
+
refreshes the installed version without npm. Retained runtimes keep pinned
|
|
78
|
+
clients working, and newer installed versions are not silently downgraded.
|
|
82
79
|
[Update and removal behavior](setup.md#update-and-remove) covers ownership
|
|
83
|
-
receipts,
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
receipts, conflict handling and explicit cleanup. `uninstall` removes managed
|
|
81
|
+
clients and npm; `--client NAME` or `--keep-package` retains the package.
|
|
82
|
+
Customized files and saved data are preserved. If interrupted setup leaves `setup.lock`,
|
|
86
83
|
confirm that no setup process is running before removing that directory locally.
|
|
87
84
|
An invalid or incomplete runtime is never selected for serving. Failed validation
|
|
88
85
|
clears its ready marker so explicit setup can rebuild it; older runtimes remain
|
|
@@ -108,10 +105,9 @@ tum-mcp scope zulip ACCOUNT_ID --channel CHANNEL_ID
|
|
|
108
105
|
`connect` takes no arguments or connection options. Move with up/down, toggle
|
|
109
106
|
services with Space, and press Enter. Choose a browser when more than one is
|
|
110
107
|
installed. Only selected tabs open in one temporary browser context. Moodle,
|
|
111
|
-
Artemis and Zulip all follow this flow
|
|
108
|
+
Artemis and Zulip all follow this flow.
|
|
112
109
|
Sign-in/MFA stays in the browser. `TERM=dumb` uses numbered prompts. Ctrl+C
|
|
113
|
-
cancels and retains completed connections.
|
|
114
|
-
`--service`/`--auth` flags are rejected before any browser opens.
|
|
110
|
+
cancels and retains completed connections.
|
|
115
111
|
|
|
116
112
|
Detection covers Chrome, Edge, Brave, Chromium, Vivaldi, Opera and selected
|
|
117
113
|
preview channels. Set `TUM_MCP_BROWSER` to an executable path for another
|
|
@@ -121,41 +117,30 @@ Only service-owned cookies are saved; identity-provider cookies are discarded
|
|
|
121
117
|
when the temporary browser closes. Existing browser profiles are never imported.
|
|
122
118
|
|
|
123
119
|
Zulip uses its read-only `/json/` routes with the browser session; no API key is
|
|
124
|
-
required. Reconnect through the same selector when a session expires.
|
|
125
|
-
|
|
126
|
-
onboarding. See [Zulip's dispatcher](https://github.com/zulip/zulip/blob/12.0/zerver/lib/rest.py).
|
|
120
|
+
required. Reconnect through the same selector when a session expires.
|
|
121
|
+
See [Zulip's dispatcher](https://github.com/zulip/zulip/blob/12.0/zerver/lib/rest.py).
|
|
127
122
|
Existing scopes survive reconnection; new identities start with empty scopes.
|
|
128
123
|
Private Zulip channels require `--private-channel ID` as well as `--channel ID`.
|
|
129
124
|
`--sensitive` enables own Artemis participation/feedback reads. It grants no
|
|
130
125
|
website mutation or local execution permission.
|
|
131
126
|
|
|
132
127
|
Settings live outside the installed package in `TUMMCP/config/preferences.json`.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
campus connections deliberately do not read or migrate native-store secrets;
|
|
136
|
-
reconnect each service once to use local-file storage. The combined MCP process
|
|
137
|
-
uses that storage for all `moodle_*` compatibility tools. The separate Moodle-only
|
|
138
|
-
installer keeps its native-store default.
|
|
139
|
-
Historical Git credentials remain local data for legacy cleanup; the current
|
|
140
|
-
package provides no assignment Git workflow.
|
|
141
|
-
|
|
142
|
-
New Moodle sessions live under `TUMMoodle/local-session/`; campus credentials live
|
|
143
|
-
under `TUMMCP/credentials/local/`. Each directory has an independent `session.key`.
|
|
128
|
+
Moodle sessions live under `TUMMoodle/local-session/`; Artemis and Zulip credentials
|
|
129
|
+
live under `TUMMCP/credentials/local/`. Each directory has an independent `session.key`.
|
|
144
130
|
Encrypted records and keys have owner-only permissions: 0700 directories / 0600
|
|
145
131
|
keys on POSIX; protected current-user DACLs on Windows through PowerShell. No
|
|
146
|
-
administrator privileges are requested. This avoids
|
|
132
|
+
administrator privileges are requested. This avoids native credential-store
|
|
147
133
|
prompts but protects less than a native store. A copy of both ciphertext and key
|
|
148
134
|
is sufficient to decrypt a session. Do not share, sync publicly, or package them.
|
|
149
135
|
|
|
150
136
|
Missing or damaged keys fail closed. Restore the matching key from a private
|
|
151
137
|
backup. To start fresh, stop clients and move the affected **local storage folder**
|
|
152
|
-
to a private backup before reconnecting
|
|
153
|
-
|
|
154
|
-
Native-store records remain available to the older installation for rollback.
|
|
138
|
+
to a private backup before reconnecting. Keep downloaded course materials when
|
|
139
|
+
repairing a login.
|
|
155
140
|
|
|
156
141
|
Zulip message text is retrieved live and is not persisted. Retrieved excerpts may
|
|
157
|
-
be transmitted to the coding agent's configured AI provider. Moodle
|
|
158
|
-
|
|
142
|
+
be transmitted to the coding agent's configured AI provider. Moodle caches
|
|
143
|
+
documents and extracted text locally in plaintext.
|
|
159
144
|
|
|
160
145
|
Course nicknames can be mapped in `mappings` using explicit per-service IDs.
|
|
161
146
|
The tools resolve one exact mapping; ambiguity never grants broader access.
|
|
@@ -191,11 +176,9 @@ progress are preserved during package updates/removal.
|
|
|
191
176
|
## Client and skill setup
|
|
192
177
|
|
|
193
178
|
`setup --client codex` prepares a local plugin/marketplace entry using the
|
|
194
|
-
portable Agent Plugins schemas. Enable it in
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
The old installation remains available for rollback. Setup reports required UI
|
|
198
|
-
steps and never equates writing files with activation.
|
|
179
|
+
portable Agent Plugins schemas. Enable it in Codex Plugins, restart, and verify
|
|
180
|
+
real tool and skill loading. Setup reports required UI steps and never equates
|
|
181
|
+
writing files with activation.
|
|
199
182
|
|
|
200
183
|
The Codex adapter uses a contained Node launcher to pin the managed Python in
|
|
201
184
|
isolated mode; portable stdio manifests require a bare or contained executable.
|
|
@@ -291,5 +274,5 @@ it is described as verified. Test data and public fixtures are synthetic. Real
|
|
|
291
274
|
course material, local agent notes and screenshots stay under ignored local paths.
|
|
292
275
|
Maintainer checks include a reviewed tarball inventory, integrity checks and an
|
|
293
276
|
isolated consumer install. Never publish or stage npm packages as a test. Registry
|
|
294
|
-
publication
|
|
277
|
+
publication is an explicit maintainer action.
|
|
295
278
|
`tum-mcp status` reports registry state as `not_checked`; it reads local configuration.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"wheel": "tum_mcp-0.6.
|
|
3
|
-
"wheelSha256": "
|
|
2
|
+
"wheel": "tum_mcp-0.6.1-py3-none-any.whl",
|
|
3
|
+
"wheelSha256": "59ecf435716e6c0ab68ed9720f2c137b3236f64dd5e962db4c3bf82b76c45ba2",
|
|
4
4
|
"requirementsSha256": "25f9efa7a4ffb0b97abcfeb9d4706d04808273ab1b35abcfaa1ef0b66e87591b"
|
|
5
5
|
}
|
|
Binary file
|
package/npm/lock.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export async function acquireLock(root, name = 'setup') {
|
|
8
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
9
|
+
const lock = join(root, `${name}.lock`);
|
|
10
|
+
const owner = { pid: process.pid, host: hostname(), nonce: randomUUID() };
|
|
11
|
+
const guard = join(root, `${name}.claim`);
|
|
12
|
+
try { await mkdir(guard, { mode: 0o700 }); }
|
|
13
|
+
catch { throw new Error(`Another ${name} is claiming the lock. Retry; an interrupted ${name}.claim needs local review.`); }
|
|
14
|
+
try {
|
|
15
|
+
try { await mkdir(lock, { mode: 0o700 }); }
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== 'EEXIST') throw error;
|
|
18
|
+
let previous;
|
|
19
|
+
try { previous = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')); }
|
|
20
|
+
catch { throw new Error(`${name} lock has no readable owner. Confirm no operation is running before removing ${name}.lock.`); }
|
|
21
|
+
if (!Number.isSafeInteger(previous.pid) || previous.pid <= 0 || previous.host !== hostname()) {
|
|
22
|
+
throw new Error(`${name} lock belongs to an unknown owner; preserve it for local review.`);
|
|
23
|
+
}
|
|
24
|
+
let alive = true;
|
|
25
|
+
try { process.kill(previous.pid, 0); }
|
|
26
|
+
catch (failure) { if (failure.code === 'ESRCH') alive = false; }
|
|
27
|
+
if (alive) throw new Error(`Another ${name} is running; retry when it finishes.`);
|
|
28
|
+
const abandoned = `${lock}.${randomUUID()}.abandoned`;
|
|
29
|
+
await rename(lock, abandoned);
|
|
30
|
+
await rm(abandoned, { recursive: true, force: true });
|
|
31
|
+
try { await mkdir(lock, { mode: 0o700 }); }
|
|
32
|
+
catch { throw new Error(`Another ${name} acquired the lock; retry when it finishes.`); }
|
|
33
|
+
}
|
|
34
|
+
const temporary = join(lock, `${owner.nonce}.pending`);
|
|
35
|
+
await writeFile(temporary, JSON.stringify(owner), { mode: 0o600, flag: 'wx' });
|
|
36
|
+
await rename(temporary, join(lock, 'owner.json'));
|
|
37
|
+
} finally { await rmdir(guard); }
|
|
38
|
+
return async () => {
|
|
39
|
+
const current = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8'));
|
|
40
|
+
if (current.nonce !== owner.nonce) throw new Error(`${name} lock ownership changed; it was preserved.`);
|
|
41
|
+
await rm(lock, { recursive: true, force: true });
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, dirname, join, basename, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const packageName = '@jamestesting888/tum-mcp';
|
|
7
|
+
|
|
8
|
+
function canonical(value) {
|
|
9
|
+
let path = resolve(value.replace(/^~(?=$|[/\\])/, homedir()));
|
|
10
|
+
const tail = [];
|
|
11
|
+
while (!existsSync(path)) { tail.unshift(basename(path)); path = dirname(path); }
|
|
12
|
+
return join(realpathSync(path), ...tail);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function packageStorage(root, dataDirectory) {
|
|
16
|
+
const home = homedir();
|
|
17
|
+
const base = process.platform === 'darwin' ? join(home, 'Library/Application Support')
|
|
18
|
+
: process.platform === 'win32' ? process.env.LOCALAPPDATA || join(home, 'AppData/Local')
|
|
19
|
+
: process.env.XDG_DATA_HOME || join(home, '.local/share');
|
|
20
|
+
const storage = {
|
|
21
|
+
TUM_MCP_DATA_DIR: canonical(dataDirectory),
|
|
22
|
+
TUM_MCP_CONFIG_DIR: canonical(process.env.TUM_MCP_CONFIG_DIR || join(dataDirectory, 'config')),
|
|
23
|
+
TUM_MOODLE_DATA_DIR: canonical(process.env.TUM_MOODLE_DATA_DIR || join(base, 'TUMMoodle')),
|
|
24
|
+
TUM_MOODLE_DOWNLOAD_DIR: canonical(process.env.TUM_MOODLE_DOWNLOAD_DIR || join(home, 'Downloads/TUM Moodle')),
|
|
25
|
+
};
|
|
26
|
+
const config = storage.TUM_MCP_CONFIG_DIR;
|
|
27
|
+
const settings = join(config, 'preferences.json');
|
|
28
|
+
let workspace = join(home, 'University');
|
|
29
|
+
if (existsSync(settings)) {
|
|
30
|
+
try {
|
|
31
|
+
const prefs = JSON.parse(readFileSync(settings, 'utf8'));
|
|
32
|
+
if (prefs.workspace_root !== undefined) {
|
|
33
|
+
if (typeof prefs.workspace_root !== 'string') throw new Error('Invalid storage path');
|
|
34
|
+
workspace = prefs.workspace_root;
|
|
35
|
+
}
|
|
36
|
+
} catch { throw new Error('Storage settings could not be verified; package files and saved data were preserved.'); }
|
|
37
|
+
}
|
|
38
|
+
for (const path of [...Object.values(storage), workspace]) {
|
|
39
|
+
const inside = relative(root, canonical(path));
|
|
40
|
+
if (inside === '' || (inside.split(sep)[0] !== '..' && !isAbsolute(inside))) {
|
|
41
|
+
throw new Error('Package installation overlaps saved data or course work. Move that data outside the package before update/removal.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return storage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function npmCli() {
|
|
48
|
+
const paths = (process.env.PATH || '').split(delimiter);
|
|
49
|
+
const candidates = [process.env.npm_execpath,
|
|
50
|
+
join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'),
|
|
51
|
+
join(dirname(dirname(process.execPath)), 'lib/node_modules/npm/bin/npm-cli.js'),
|
|
52
|
+
...paths.flatMap(path => [join(path, 'npm'), join(path, 'node_modules/npm/bin/npm-cli.js')])];
|
|
53
|
+
for (const path of candidates) {
|
|
54
|
+
if (!path || !existsSync(path)) continue;
|
|
55
|
+
const resolved = realpathSync(path);
|
|
56
|
+
if (basename(resolved) !== 'npm-cli.js') continue;
|
|
57
|
+
const manifest = join(dirname(dirname(resolved)), 'package.json');
|
|
58
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, 'utf8')).name === 'npm') return resolved;
|
|
59
|
+
}
|
|
60
|
+
throw new Error('npm could not be located beside Node or on PATH. Install Node.js with npm, then retry.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function runNpm(cli, args, capture = false) {
|
|
64
|
+
const result = spawnSync(process.execPath, [cli, ...args, '--ignore-scripts', '--no-audit', '--no-fund', '--no-update-notifier'], {
|
|
65
|
+
shell: false, stdio: capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', encoding: 'utf8', timeout: 600000,
|
|
66
|
+
});
|
|
67
|
+
if (result.error || result.status !== 0) throw new Error('npm operation failed. Saved data and retained client runtimes were preserved; resolve the npm error and retry.');
|
|
68
|
+
return capture ? result.stdout.trim() : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function installationTarget(root) {
|
|
72
|
+
const modules = dirname(dirname(root));
|
|
73
|
+
if (basename(modules) !== 'node_modules' || modules.split(/[\\/]/).some(part => ['.pnpm', '_npx'].includes(part))) {
|
|
74
|
+
throw new Error('Package management requires an npm installation. Use update --local for a source/pinned build.');
|
|
75
|
+
}
|
|
76
|
+
if (`${basename(dirname(root))}/${basename(root)}` !== packageName) {
|
|
77
|
+
throw new Error('Automatic package management requires the original package name; aliases were preserved.');
|
|
78
|
+
}
|
|
79
|
+
const parent = dirname(modules);
|
|
80
|
+
const prefix = process.platform === 'win32' ? parent : dirname(parent);
|
|
81
|
+
const shim = process.platform === 'win32' ? join(prefix, 'tum-mcp.cmd') : join(prefix, 'bin/tum-mcp');
|
|
82
|
+
if (existsSync(shim)) {
|
|
83
|
+
const owned = process.platform === 'win32'
|
|
84
|
+
? readFileSync(shim, 'utf8').replaceAll('\\', '/').includes('node_modules/@jamestesting888/tum-mcp/npm/tum-mcp.mjs')
|
|
85
|
+
: realpathSync(shim) === join(root, 'npm/tum-mcp.mjs');
|
|
86
|
+
if (owned && (process.platform === 'win32' || basename(parent) === 'lib')) return { prefix, global: true };
|
|
87
|
+
}
|
|
88
|
+
const manifest = join(parent, 'package.json');
|
|
89
|
+
if (existsSync(manifest)) {
|
|
90
|
+
const project = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
91
|
+
if (['dependencies', 'devDependencies', 'optionalDependencies'].some(key => project[key]?.[packageName])) {
|
|
92
|
+
return { prefix: parent, global: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new Error('The npm installation prefix could not be verified; package files were preserved.');
|
|
96
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { copyFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { acquireLock } from './lock.mjs';
|
|
8
|
+
import { installationTarget, npmCli, packageName, packageStorage, runNpm } from './npm-command.mjs';
|
|
9
|
+
|
|
10
|
+
function runLauncher(root, args, quiet = false) {
|
|
11
|
+
const result = spawnSync(process.execPath, [join(root, 'npm/tum-mcp.mjs'), ...args], {
|
|
12
|
+
stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit', encoding: 'utf8', shell: false, timeout: 600000,
|
|
13
|
+
});
|
|
14
|
+
if (result.error || result.status !== 0) {
|
|
15
|
+
if (quiet) process.stderr.write((result.stdout || '') + (result.stderr || ''));
|
|
16
|
+
throw new Error(`${args[0]} did not complete. Saved data and customized client files were preserved; review the details and retry.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function versionParts(version) {
|
|
21
|
+
const match = typeof version === 'string' && version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/);
|
|
22
|
+
if (!match) throw new Error('npm returned an invalid package version.');
|
|
23
|
+
const prerelease = match[4]?.split('.') || [];
|
|
24
|
+
if (prerelease.some(part => /^0\d+$/.test(part))) throw new Error('npm returned an invalid prerelease version.');
|
|
25
|
+
return { core: match.slice(1, 4).map(BigInt), prerelease };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function newerVersion(latest, current) {
|
|
29
|
+
const left = versionParts(latest), right = versionParts(current);
|
|
30
|
+
for (let i = 0; i < left.core.length; i++) if (left.core[i] !== right.core[i]) return left.core[i] > right.core[i];
|
|
31
|
+
const a = left.prerelease, b = right.prerelease;
|
|
32
|
+
if (!a.length || !b.length) return !a.length && Boolean(b.length);
|
|
33
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
34
|
+
if (a[i] === undefined) return false;
|
|
35
|
+
if (b[i] === undefined) return true;
|
|
36
|
+
if (a[i] === b[i]) continue;
|
|
37
|
+
const numericA = /^\d+$/.test(a[i]), numericB = /^\d+$/.test(b[i]);
|
|
38
|
+
if (numericA && numericB) return BigInt(a[i]) > BigInt(b[i]);
|
|
39
|
+
if (numericA !== numericB) return numericB;
|
|
40
|
+
return a[i] > b[i];
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function operate(job) {
|
|
46
|
+
const release = await acquireLock(job.target.prefix, 'tum-mcp-package');
|
|
47
|
+
try {
|
|
48
|
+
const scope = ['--prefix', job.target.prefix, `--global=${job.target.global}`];
|
|
49
|
+
if (job.action === 'uninstall') {
|
|
50
|
+
const configured = existsSync(join(job.dataDirectory, 'runtime.json')) ||
|
|
51
|
+
existsSync(join(job.configDirectory, 'clients'));
|
|
52
|
+
if (configured || job.args.some(arg => ['--purge-runtime', '--forget-credentials'].includes(arg))) {
|
|
53
|
+
runLauncher(job.root, ['uninstall', '--keep-package', ...job.args]);
|
|
54
|
+
}
|
|
55
|
+
runNpm(job.npm, ['uninstall', ...scope, packageName]);
|
|
56
|
+
console.log('TUM MCP package and managed client setup removed. Saved user data was retained unless explicitly requested.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const current = JSON.parse(readFileSync(join(job.root, 'package.json'), 'utf8')).version;
|
|
60
|
+
const latest = JSON.parse(runNpm(job.npm, ['view', `${packageName}@latest`, 'version', '--json', ...scope], true));
|
|
61
|
+
if (!newerVersion(latest, current)) {
|
|
62
|
+
console.log(`Installed ${current}; no newer npm release. Refreshing configured clients.`);
|
|
63
|
+
runLauncher(job.root, ['update', '--local']);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`Preparing TUM MCP ${latest}.`);
|
|
67
|
+
const packed = JSON.parse(runNpm(job.npm, ['pack', `${packageName}@${latest}`, '--pack-destination', job.temporary, '--json', ...scope], true));
|
|
68
|
+
if (!Array.isArray(packed) || packed.length !== 1 || packed[0].name !== packageName || packed[0].version !== latest || basename(packed[0].filename) !== packed[0].filename) {
|
|
69
|
+
throw new Error('npm package metadata did not match the selected release.');
|
|
70
|
+
}
|
|
71
|
+
const archive = join(job.temporary, packed[0].filename);
|
|
72
|
+
const staging = join(job.temporary, 'prepared');
|
|
73
|
+
runNpm(job.npm, ['install', '--prefix', staging, '--global=false', archive], true);
|
|
74
|
+
const prepared = join(staging, 'node_modules', packageName);
|
|
75
|
+
runLauncher(prepared, ['setup'], true);
|
|
76
|
+
// Only a healthy prepared release may replace the user's npm installation.
|
|
77
|
+
runNpm(job.npm, ['install', ...scope, job.target.global ? archive : `${packageName}@${latest}`]);
|
|
78
|
+
if (JSON.parse(readFileSync(join(job.root, 'package.json'), 'utf8')).version !== latest) throw new Error('npm did not install the selected version; client activation was stopped.');
|
|
79
|
+
runLauncher(job.root, ['update', '--local']);
|
|
80
|
+
console.log(`Updated TUM MCP to ${latest}. Restart clients to load refreshed skills.`);
|
|
81
|
+
} finally { await release(); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function managePackage(action, args, dataDirectory) {
|
|
85
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
86
|
+
const inside = relative(root, process.cwd());
|
|
87
|
+
if (inside === '' || (inside.split(sep)[0] !== '..' && !isAbsolute(inside))) throw new Error('Run package update/removal from outside the installed package directory.');
|
|
88
|
+
const target = installationTarget(root);
|
|
89
|
+
const storage = packageStorage(root, dataDirectory);
|
|
90
|
+
const configDirectory = storage.TUM_MCP_CONFIG_DIR;
|
|
91
|
+
const npm = npmCli();
|
|
92
|
+
const temporary = await mkdtemp(join(tmpdir(), 'tum-mcp-package-'));
|
|
93
|
+
try {
|
|
94
|
+
for (const name of ['package-manager.mjs', 'npm-command.mjs', 'lock.mjs']) await copyFile(join(root, 'npm', name), join(temporary, name));
|
|
95
|
+
const job = { action, args, root, dataDirectory, configDirectory, target, npm, temporary };
|
|
96
|
+
const path = join(temporary, 'job.json');
|
|
97
|
+
await writeFile(path, JSON.stringify(job), { mode: 0o600, flag: 'wx' });
|
|
98
|
+
// This worker and its cwd survive Windows replacing/removing the package files.
|
|
99
|
+
const result = spawnSync(process.execPath, [join(temporary, 'package-manager.mjs'), '--job', path], {
|
|
100
|
+
cwd: temporary, stdio: 'inherit', shell: false, env: { ...process.env, ...storage },
|
|
101
|
+
});
|
|
102
|
+
return result.status ?? (result.signal === 'SIGINT' ? 130 : 2);
|
|
103
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (process.argv[1] && process.argv[2] === '--job' && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
107
|
+
try { await operate(JSON.parse(readFileSync(process.argv[3], 'utf8'))); }
|
|
108
|
+
catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
109
|
+
}
|
package/npm/runtime.mjs
CHANGED
|
@@ -2,10 +2,11 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { chmod, mkdir, mkdtemp, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { homedir
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
6
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { download } from './download.mjs';
|
|
9
|
+
import { acquireLock } from './lock.mjs';
|
|
9
10
|
|
|
10
11
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
12
|
const pins = JSON.parse(readFileSync(join(packageRoot, 'npm/uv-artifacts.json'), 'utf8'));
|
|
@@ -109,40 +110,7 @@ async function uvExecutable() {
|
|
|
109
110
|
return cached;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
export
|
|
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
|
-
}
|
|
113
|
+
export function acquireSetupLock() { return acquireLock(dataRoot()); }
|
|
146
114
|
|
|
147
115
|
export async function ensureRuntime() {
|
|
148
116
|
const backend = join(packageRoot, 'npm/backend');
|
package/npm/tum-mcp.mjs
CHANGED
|
@@ -1,33 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { acquireSetupLock, currentPython, dataRoot, ensureRuntime, removeRuntimeFiles } from './runtime.mjs';
|
|
6
|
+
import { managePackage } from './package-manager.mjs';
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
let args = process.argv.slice(2);
|
|
7
9
|
const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
8
10
|
try {
|
|
9
11
|
if (args.length === 0 || (args.length === 1 && ['--help', '-h'].includes(args[0]))) {
|
|
10
12
|
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.`);
|
|
11
13
|
} else if (['--version', 'version'].includes(args[0])) {
|
|
12
14
|
console.log(version);
|
|
13
|
-
} else if (['setup', 'update'].includes(args[0]) && args.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
: 'Usage: tum-mcp setup [--client codex|claude|cursor|antigravity|generic] [--host local|windows]\nProvision the managed runtime and register explicitly selected clients.'
|
|
15
|
+
} else if (['setup', 'update', 'uninstall'].includes(args[0]) && args.some(arg => ['--help', '-h'].includes(arg))) {
|
|
16
|
+
const help = {
|
|
17
|
+
update: 'Usage: tum-mcp update [--local]\nInstall the latest npm release and refresh configured clients.\n--local refreshes this installed version without contacting npm.',
|
|
18
|
+
setup: 'Usage: tum-mcp setup [--client codex|claude|cursor|antigravity|generic] [--host local|windows]\nProvision the managed runtime and register explicitly selected clients.',
|
|
19
|
+
uninstall: 'Usage: tum-mcp uninstall [--all] [--purge-runtime] [--forget-credentials]\nRemove managed client setup and this npm installation. Saved data is retained by default.\n--client NAME removes one client only. --keep-package keeps npm installed.',
|
|
20
|
+
};
|
|
21
|
+
console.log(help[args[0]]);
|
|
17
22
|
} else {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
23
|
+
let packageAction = args[0] === 'update';
|
|
24
|
+
if (args[0] === 'update') {
|
|
25
|
+
if (args.length > 2 || (args.length === 2 && args[1] !== '--local')) throw new Error('Usage: tum-mcp update [--local]. Use --help for details.');
|
|
26
|
+
if (args[1] === '--local') { packageAction = false; args = ['update']; }
|
|
27
|
+
}
|
|
28
|
+
if (args[0] === 'uninstall') {
|
|
29
|
+
const { values } = parseArgs({ args: args.slice(1), options: {
|
|
30
|
+
all: { type: 'boolean' }, client: { type: 'string' }, host: { type: 'string' },
|
|
31
|
+
'keep-package': { type: 'boolean' }, 'purge-runtime': { type: 'boolean' }, 'forget-credentials': { type: 'boolean' },
|
|
32
|
+
} });
|
|
33
|
+
if (values.host && !['local', 'windows'].includes(values.host)) throw new Error('Choose --host local or windows.');
|
|
34
|
+
if (values.client !== undefined && !['codex', 'claude', 'cursor', 'antigravity', 'generic'].includes(values.client)) throw new Error('Choose a supported --client name.');
|
|
35
|
+
if (values.client && (values.all || values['purge-runtime'] || values['forget-credentials'])) throw new Error('Client-only removal cannot use --all or data/runtime cleanup options.');
|
|
36
|
+
if (!values.client && !values.all) args.push('--all');
|
|
37
|
+
packageAction = !values.client && !values['keep-package'];
|
|
38
|
+
args = args.filter(arg => arg !== '--keep-package');
|
|
39
|
+
}
|
|
40
|
+
if (packageAction) {
|
|
41
|
+
process.exitCode = await managePackage(args[0], args.slice(1), dataRoot());
|
|
42
|
+
} else {
|
|
43
|
+
let python = ['setup', 'update'].includes(args[0]) ? await ensureRuntime() : currentPython();
|
|
44
|
+
if (args[0] === 'uninstall' && !python) python = await ensureRuntime();
|
|
45
|
+
if (!python) throw new Error('SETUP_REQUIRED: Run tum-mcp setup first.');
|
|
46
|
+
const forwarded = args;
|
|
47
|
+
const env = { ...process.env, TUM_MCP_NPM_LAUNCHER: '1' }; delete env.PYTHONPATH; delete env.PYTHONHOME;
|
|
48
|
+
const purge = args[0] === 'uninstall' && args.includes('--purge-runtime');
|
|
49
|
+
const release = purge ? await acquireSetupLock() : null;
|
|
50
|
+
try {
|
|
51
|
+
const child = spawnSync(python, ['-I', '-m', 'tum_mcp', ...forwarded], { env, stdio: 'inherit', shell: false });
|
|
52
|
+
if (child.error) throw new Error('Managed backend could not start; run setup again.');
|
|
53
|
+
process.exitCode = child.status ?? (child.signal === 'SIGINT' ? 130 : 1);
|
|
54
|
+
if (purge && child.status === 0) await removeRuntimeFiles();
|
|
55
|
+
} finally { if (release) await release(); }
|
|
56
|
+
}
|
|
32
57
|
}
|
|
33
58
|
} catch (error) { console.error(error.message); process.exitCode = 2; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamestesting888/tum-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Read-only TUM Moodle, Artemis and Zulip integration with source-backed study workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"npm/tum-mcp.mjs",
|
|
15
15
|
"npm/runtime.mjs",
|
|
16
16
|
"npm/download.mjs",
|
|
17
|
+
"npm/lock.mjs",
|
|
18
|
+
"npm/npm-command.mjs",
|
|
19
|
+
"npm/package-manager.mjs",
|
|
17
20
|
"npm/uv-artifacts.json",
|
|
18
21
|
"npm/backend/*.whl",
|
|
19
22
|
"npm/backend/requirements.txt",
|
|
@@ -31,7 +34,8 @@
|
|
|
31
34
|
"prepack": "node npm/build.mjs",
|
|
32
35
|
"test": "node --test npm/test/*.test.mjs",
|
|
33
36
|
"test:consumer": "node npm/smoke-consumer.mjs",
|
|
34
|
-
"test:upgrade": "node npm/upgrade-consumer.mjs"
|
|
37
|
+
"test:upgrade": "node npm/upgrade-consumer.mjs",
|
|
38
|
+
"test:package-lifecycle": "node npm/package-consumer.mjs"
|
|
35
39
|
},
|
|
36
40
|
"publishConfig": {
|
|
37
41
|
"access": "public"
|