@creeperhost/modlens-mcp 1.6.20 → 1.6.22
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/ATTRIBUTION.md +8 -1
- package/README.md +77 -2
- package/RUNTIME.md +262 -0
- package/TESTING.md +8 -0
- package/dist/hosted-mod-license.d.ts +36 -0
- package/dist/hosted-mod-license.d.ts.map +1 -0
- package/dist/hosted-mod-license.js +65 -0
- package/dist/hosted-mod-license.js.map +1 -0
- package/dist/hosted-oauth.d.ts +36 -0
- package/dist/hosted-oauth.d.ts.map +1 -0
- package/dist/hosted-oauth.js +522 -0
- package/dist/hosted-oauth.js.map +1 -0
- package/dist/hosted-policy.d.ts +36 -0
- package/dist/hosted-policy.d.ts.map +1 -0
- package/dist/hosted-policy.js +354 -0
- package/dist/hosted-policy.js.map +1 -0
- package/dist/launcher.js +104 -89
- package/dist/launcher.js.map +1 -1
- package/dist/license-notices.d.ts +8 -0
- package/dist/license-notices.d.ts.map +1 -0
- package/dist/license-notices.js +10 -0
- package/dist/license-notices.js.map +1 -0
- package/dist/license-source-references.d.ts +12 -0
- package/dist/license-source-references.d.ts.map +1 -0
- package/dist/license-source-references.js +18 -0
- package/dist/license-source-references.js.map +1 -0
- package/dist/license-templates.d.ts +6 -0
- package/dist/license-templates.d.ts.map +1 -0
- package/dist/license-templates.js +55 -0
- package/dist/license-templates.js.map +1 -0
- package/dist/local-mod.d.ts +31 -0
- package/dist/local-mod.d.ts.map +1 -0
- package/dist/local-mod.js +71 -0
- package/dist/local-mod.js.map +1 -0
- package/dist/minecraft.d.ts +2 -0
- package/dist/minecraft.d.ts.map +1 -1
- package/dist/minecraft.js +16 -0
- package/dist/minecraft.js.map +1 -1
- package/dist/mod-license.d.ts +53 -0
- package/dist/mod-license.d.ts.map +1 -0
- package/dist/mod-license.js +617 -0
- package/dist/mod-license.js.map +1 -0
- package/dist/repositories/mcVersion.d.ts +1 -0
- package/dist/repositories/mcVersion.d.ts.map +1 -1
- package/dist/repositories/mcVersion.js +11 -0
- package/dist/repositories/mcVersion.js.map +1 -1
- package/dist/runtime/cli.d.ts +2 -0
- package/dist/runtime/cli.d.ts.map +1 -0
- package/dist/runtime/cli.js +163 -0
- package/dist/runtime/cli.js.map +1 -0
- package/dist/runtime/companion.d.ts +32 -0
- package/dist/runtime/companion.d.ts.map +1 -0
- package/dist/runtime/companion.js +135 -0
- package/dist/runtime/companion.js.map +1 -0
- package/dist/runtime/guidance.d.ts +73 -0
- package/dist/runtime/guidance.d.ts.map +1 -0
- package/dist/runtime/guidance.js +40 -0
- package/dist/runtime/guidance.js.map +1 -0
- package/dist/runtime/hub.d.ts +150 -0
- package/dist/runtime/hub.d.ts.map +1 -0
- package/dist/runtime/hub.js +561 -0
- package/dist/runtime/hub.js.map +1 -0
- package/dist/runtime/modlens-agent.jar +0 -0
- package/dist/runtime/protocol.d.ts +202 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +91 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/requests.d.ts +367 -0
- package/dist/runtime/requests.d.ts.map +1 -0
- package/dist/runtime/requests.js +57 -0
- package/dist/runtime/requests.js.map +1 -0
- package/dist/server.js +186 -39
- package/dist/server.js.map +1 -1
- package/dist/tools/hosted-mc-index.d.ts +8 -0
- package/dist/tools/hosted-mc-index.d.ts.map +1 -0
- package/dist/tools/hosted-mc-index.js +92 -0
- package/dist/tools/hosted-mc-index.js.map +1 -0
- package/dist/tools/mc-fts.d.ts +2 -0
- package/dist/tools/mc-fts.d.ts.map +1 -1
- package/dist/tools/mc-fts.js +8 -0
- package/dist/tools/mc-fts.js.map +1 -1
- package/dist/tools/project.d.ts +7 -3
- package/dist/tools/project.d.ts.map +1 -1
- package/dist/tools/project.js +45 -3
- package/dist/tools/project.js.map +1 -1
- package/dist/tools/report-issue.d.ts +77 -0
- package/dist/tools/report-issue.d.ts.map +1 -0
- package/dist/tools/report-issue.js +92 -0
- package/dist/tools/report-issue.js.map +1 -0
- package/dist/tools/runtime.d.ts +6 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +18 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/source.d.ts +1 -1
- package/dist/tools/source.d.ts.map +1 -1
- package/dist/tools/source.js +5 -1
- package/dist/tools/source.js.map +1 -1
- package/dist/tools/vanilla.d.ts +30 -1
- package/dist/tools/vanilla.d.ts.map +1 -1
- package/dist/tools/vanilla.js +184 -25
- package/dist/tools/vanilla.js.map +1 -1
- package/package.json +5 -1
- package/scripts/build-runtime-agent.mjs +64 -0
- package/scripts/fixtures/mod-license-corpus.json +154 -0
- package/scripts/gradle/modlens-runtime.init.gradle +17 -0
- package/scripts/runtime-test-client.mjs +36 -0
- package/scripts/test-hosted-oauth.mjs +186 -0
- package/scripts/test-hosted-policy.mjs +182 -0
- package/scripts/test-mod-licenses-live.mjs +86 -0
- package/scripts/test-package.mjs +44 -0
- package/scripts/test-project-http.mjs +46 -3
- package/scripts/test-runtime-gradle.mjs +107 -0
- package/scripts/test-runtime-minecraft.mjs +190 -0
- package/scripts/test-runtime-native.mjs +120 -0
- package/scripts/test-runtime-remote.mjs +209 -0
- package/scripts/update-license-templates.mjs +14 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "ModMenu",
|
|
4
|
+
"version": "v6Xx3fbU",
|
|
5
|
+
"release": "11.0.4",
|
|
6
|
+
"hash": "a88e0d67c82b0434cbf56776c482587c275ed660f635d33f53ba485dfa30dcc3",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"expected": "hosted_allowed",
|
|
9
|
+
"target": "com.terraformersmc.modmenu.ModMenu"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "FerriteCore",
|
|
13
|
+
"version": "sOzRw3CG",
|
|
14
|
+
"release": "7.0.3-fabric",
|
|
15
|
+
"hash": "98c3ab1d5aab8f14b5d082d3f1a467727fd335f02d124555c94339761c4c18f2",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"expected": "hosted_allowed",
|
|
18
|
+
"target": "malte0811.ferritecore.IPlatformHooks"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "MouseTweaks",
|
|
22
|
+
"version": "9I21YYxf",
|
|
23
|
+
"release": "1.21-2.26.1-neoforge",
|
|
24
|
+
"hash": "68e6f4201c5de97b77929a7215c9552495696ca6a3bf3ae4eacc34e135f6cc8b",
|
|
25
|
+
"license": "BSD-3-Clause",
|
|
26
|
+
"expected": "hosted_allowed",
|
|
27
|
+
"target": "yalter.mousetweaks.Constants"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "DynamicFPS",
|
|
31
|
+
"version": "LqevwKp5",
|
|
32
|
+
"release": "3.11.4",
|
|
33
|
+
"hash": "0929d2b9e164a08d25a7ba8a5f0750081b66dea9171cec52d2f0fdc792fc7e5e",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"expected": "hosted_allowed",
|
|
36
|
+
"target": "architectury_inject_dynamicfps_common_96ad272d90fa4fcc8d67753a3449cf6d_48138490a95625983251b45ac7336d8a7e463234a1fad5dfdc4c144b1c5f439adynamicfps3114devjar.PlatformMethods"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "Controlling",
|
|
40
|
+
"version": "FaNppCJJ",
|
|
41
|
+
"release": "19.0.5",
|
|
42
|
+
"hash": "cc525fb6b030d9e4d33176983f278919e17b33935e2ca568929bdfa98f2b44a0",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"expected": "hosted_allowed",
|
|
45
|
+
"target": "com.blamejared.controlling.Controlling"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Clumps",
|
|
49
|
+
"version": "jo7lDoK4",
|
|
50
|
+
"release": "19.0.0.1",
|
|
51
|
+
"hash": "b524ccdace2ef8fd19f5b2074f7de1103ac5065c52553f064c00e098346c293e",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"expected": "hosted_allowed",
|
|
54
|
+
"target": "com.blamejared.clumps.helper.IClumpedOrb"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "AppleSkin",
|
|
58
|
+
"version": "uAKA6Laj",
|
|
59
|
+
"release": "3.0.9+mc1.21",
|
|
60
|
+
"hash": "38b48dd6231341c9f964ce6e42c57ec866c6cc8f72bec938390a59bafb3922df",
|
|
61
|
+
"license": "Unlicense",
|
|
62
|
+
"expected": "hosted_allowed",
|
|
63
|
+
"target": "squeek.appleskin.ModInfo"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "Architectury",
|
|
67
|
+
"version": "1IiqEQGl",
|
|
68
|
+
"release": "13.0.11+neoforge",
|
|
69
|
+
"hash": "9cc92f2c09533fc5482c60f993bd891c655a0e32b637037cdcb7c2b23adedeeb",
|
|
70
|
+
"license": "LGPL-3.0-only",
|
|
71
|
+
"expected": "local_only",
|
|
72
|
+
"target": "architectury_inject_architectury_common_d9fb27da44a148dfa43566d20897d6de_f4ed98e062c0e85c5cc998c6fbbab16b146fa1f727cd0f469b44c896e896070earchitectury13011devjar.PlatformMethods"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "ClothConfig",
|
|
76
|
+
"version": "izKINKFg",
|
|
77
|
+
"release": "15.0.140+neoforge",
|
|
78
|
+
"hash": "65e722e0d98431a07c45f8bdd8d529a217cc8c175fde1740248bd5c1b4f3c0d4",
|
|
79
|
+
"license": "LGPL-3.0-only",
|
|
80
|
+
"expected": "local_only",
|
|
81
|
+
"target": "architectury_inject_clothconfig_common_d551de04735c42148ec07a78c43007d1_c487a9cf12e7d5bea169da255b814f03602bdeb8b51cff67162252c24f285789clothconfig150140devjar.PlatformMethods"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "ModernFix",
|
|
85
|
+
"version": "5HLHxQ2F",
|
|
86
|
+
"release": "5.27.24+mc1.21.1",
|
|
87
|
+
"hash": "e6e9446890f0feb3aab3f6e73ae18cb17575c370f232179fef7baa30e61538fe",
|
|
88
|
+
"license": "LGPL-3.0-only",
|
|
89
|
+
"expected": "local_only",
|
|
90
|
+
"target": "org.embeddedt.modernfix.ModernFix"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "Spark",
|
|
94
|
+
"version": "v5qtqRQi",
|
|
95
|
+
"release": "1.10.124-neoforge-1.21.1",
|
|
96
|
+
"hash": "647e8a81afbe414dba1df4ba15fd06c5d32d4cb544e68828405e8e074c2e16db",
|
|
97
|
+
"license": "GPL-3.0-only",
|
|
98
|
+
"expected": "local_only",
|
|
99
|
+
"target": "me.lucko.spark.neoforge.NeoForgePlatformInfo"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "Sodium",
|
|
103
|
+
"version": "uMOpc5uV",
|
|
104
|
+
"release": "mc1.21.1-0.8.13-neoforge",
|
|
105
|
+
"hash": "575d187b15328d7bddd8a9b1bee64333c22c14735f5780ec49eac0cd5503c08d",
|
|
106
|
+
"license": "LicenseRef-Polyform-Shield-1.0.0",
|
|
107
|
+
"expected": "local_only",
|
|
108
|
+
"target": "net.caffeinemc.mods.sodium.service.SodiumServiceModLocator"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "Iris",
|
|
112
|
+
"version": "t3ruzodq",
|
|
113
|
+
"release": "1.8.12+1.21.1-neoforge",
|
|
114
|
+
"hash": "3e3f94d2d5cdcb8145c4385d521974aa2d8669b2cc212351f70cf06488182e83",
|
|
115
|
+
"license": "LGPL-3.0-only",
|
|
116
|
+
"expected": "local_only",
|
|
117
|
+
"target": "de.odysseus.ithaka.digraph.Digraph"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "Lithium",
|
|
121
|
+
"version": "N08Z8wog",
|
|
122
|
+
"release": "mc1.21.1-0.15.4-fabric",
|
|
123
|
+
"hash": "92329d98c57f5a22091a0adb509fdb1d60113a5602c708b799b13134bad13baa",
|
|
124
|
+
"license": "LGPL-3.0-only",
|
|
125
|
+
"expected": "local_only",
|
|
126
|
+
"target": "net.caffeinemc.mods.lithium.api.inventory.LithiumCooldownReceivingInventory"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "EntityCulling",
|
|
130
|
+
"version": "8w8FfUPs",
|
|
131
|
+
"release": "1.11.2",
|
|
132
|
+
"hash": "3983b981da4a0583c7e58184a8631a23e56cc3b81f4cb53491ca600382879b82",
|
|
133
|
+
"license": "LicenseRef-tr7zw-Protective-License",
|
|
134
|
+
"expected": "local_only",
|
|
135
|
+
"target": "com.logisticscraft.occlusionculling.DataProvider"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "JourneyMap",
|
|
139
|
+
"version": "CtFE7hwT",
|
|
140
|
+
"release": "1.21.1-6.0.9+neoforge",
|
|
141
|
+
"hash": "9bd7734542233d5281750572f17a9258449e826650c081e9a67dc426262c2fde",
|
|
142
|
+
"license": "LicenseRef-All-Rights-Reserved",
|
|
143
|
+
"expected": "local_only",
|
|
144
|
+
"target": "earcut4j.Earcut"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "FabricAPI",
|
|
148
|
+
"version": "Mys3P7lK",
|
|
149
|
+
"release": "0.116.17+1.21.1",
|
|
150
|
+
"hash": "79ac44b40780acbd884b34c50be1e39af682847e5f5cb3b1fddeeaa768dce800",
|
|
151
|
+
"license": "Apache-2.0",
|
|
152
|
+
"expected": "hosted_allowed"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Opt-in launch only. Never instrument Gradle itself or every JavaExec task.
|
|
2
|
+
gradle.projectsEvaluated {
|
|
3
|
+
def configFile = new File(gradle.rootProject.projectDir, '.modlens/runtime/launch.properties')
|
|
4
|
+
if (!configFile.isFile()) throw new GradleException('Call ModLens runtime.setup for this project first')
|
|
5
|
+
def config = new Properties()
|
|
6
|
+
configFile.withInputStream { config.load(it) }
|
|
7
|
+
def taskPath = config.getProperty('task')
|
|
8
|
+
if (!taskPath.startsWith(':')) taskPath = ':' + taskPath
|
|
9
|
+
def target = gradle.rootProject.tasks.findByPath(taskPath)
|
|
10
|
+
if (!(target instanceof JavaExec)) {
|
|
11
|
+
throw new GradleException("ModLens requires a JavaExec dev run task; ${taskPath} is ${target?.class?.name ?: 'missing'}. Use every returned vmOptions entry on the actual game launch instead.")
|
|
12
|
+
}
|
|
13
|
+
target.jvmArgs("-javaagent:${config.getProperty('agent')}=${config.getProperty('connection')}")
|
|
14
|
+
// Minecraft 26.3 client startup needs this even without the agent.
|
|
15
|
+
target.jvmArgs('-XX:StackShadowPages=32')
|
|
16
|
+
target.jvmArgs("-XX:ErrorFile=${new File(configFile.parentFile, 'hs_err_pid%p.log').absolutePath}")
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Test adapter: run the same fixture through separate CLI processes instead of
|
|
2
|
+
// importing RuntimeHub. Each call reconnects to the persistent companion.
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export function runtimeTestClient(root) {
|
|
10
|
+
const launcher = fileURLToPath(new URL("../dist/launcher.js", import.meta.url));
|
|
11
|
+
const env = { ...process.env, MODLENS_CACHE_ROOT: join(root, "helper-cache") };
|
|
12
|
+
const run = (args) =>
|
|
13
|
+
JSON.parse(
|
|
14
|
+
execFileSync(process.execPath, [launcher, "--runtime", ...args], {
|
|
15
|
+
env,
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
timeout: 45000,
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
async function request(body) {
|
|
21
|
+
const file = join(root, `helper-${randomUUID()}.json`);
|
|
22
|
+
await writeFile(file, JSON.stringify(body));
|
|
23
|
+
return run(["--request-file", file]);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
setup: (projectDir, mode, gradleTask, minecraftHooks) =>
|
|
27
|
+
request({ action: "setup", projectDir, mode, gradleTask, minecraftHooks }),
|
|
28
|
+
list: () => request({ action: "sessions" }),
|
|
29
|
+
events: (afterCursor, waitMs, sessionId) =>
|
|
30
|
+
request({ action: "events", afterCursor, waitMs, sessionId }),
|
|
31
|
+
command: (sessionId, command) => request({ action: "command", sessionId, command }),
|
|
32
|
+
artifact: (sessionId, artifactName) => request({ action: "artifact", sessionId, artifactName }),
|
|
33
|
+
launch: (projectId, javaHome) => request({ action: "launch", projectId, javaHome }),
|
|
34
|
+
close: async () => run(["stop"]),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { once } from 'node:events';
|
|
6
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const root = await mkdtemp(join(tmpdir(), 'modlens-oauth-test-'));
|
|
12
|
+
const entry = fileURLToPath(new URL('../dist/server.js', import.meta.url));
|
|
13
|
+
const port = async () => {
|
|
14
|
+
const socket = createServer();
|
|
15
|
+
socket.listen(0, '127.0.0.1');
|
|
16
|
+
await once(socket, 'listening');
|
|
17
|
+
const value = socket.address().port;
|
|
18
|
+
await new Promise(resolve => socket.close(resolve));
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
const providerPort = await port();
|
|
22
|
+
const mcpPort = await port();
|
|
23
|
+
const issuer = `http://127.0.0.1:${providerPort}`;
|
|
24
|
+
const resource = `http://127.0.0.1:${mcpPort}/mcp`;
|
|
25
|
+
const profiles = new Map();
|
|
26
|
+
let partner = true;
|
|
27
|
+
let subject = 'customer:42';
|
|
28
|
+
let serial = 0;
|
|
29
|
+
const provider = createServer(async (req, res) => {
|
|
30
|
+
const url = new URL(req.url, issuer);
|
|
31
|
+
const write = (status, data) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); };
|
|
32
|
+
if (url.pathname === '/.well-known/oauth-authorization-server') return write(200, {
|
|
33
|
+
issuer, authorization_endpoint: issuer + '/authorize', token_endpoint: issuer + '/token',
|
|
34
|
+
authorization_response_iss_parameter_supported: true,
|
|
35
|
+
});
|
|
36
|
+
if (url.pathname === '/authorize') {
|
|
37
|
+
const callback = new URL(url.searchParams.get('redirect_uri'));
|
|
38
|
+
callback.searchParams.set('code', 'fake-code');
|
|
39
|
+
callback.searchParams.set('state', url.searchParams.get('state'));
|
|
40
|
+
callback.searchParams.set('iss', issuer);
|
|
41
|
+
res.writeHead(302, { Location: callback.toString() }); res.end(); return;
|
|
42
|
+
}
|
|
43
|
+
if (url.pathname === '/token') {
|
|
44
|
+
let raw = '';
|
|
45
|
+
for await (const chunk of req) raw += chunk;
|
|
46
|
+
const data = new URLSearchParams(raw);
|
|
47
|
+
if (data.get('grant_type') === 'refresh_token' && !data.get('refresh_token')?.startsWith('upstream-refresh-'))
|
|
48
|
+
return write(400, { error: 'invalid_grant' });
|
|
49
|
+
if (data.get('grant_type') === 'authorization_code' && data.get('code') !== 'fake-code')
|
|
50
|
+
return write(400, { error: 'invalid_grant' });
|
|
51
|
+
const access = `upstream-${++serial}`;
|
|
52
|
+
profiles.set(access, { sub: subject, is_partner: partner });
|
|
53
|
+
return write(200, { access_token: access, refresh_token: `upstream-refresh-${serial}`, token_type: 'Bearer', expires_in: 300 });
|
|
54
|
+
}
|
|
55
|
+
if (url.pathname === '/profile') {
|
|
56
|
+
const profile = profiles.get(req.headers.authorization?.slice(7));
|
|
57
|
+
return profile ? write(200, { ...profile, is_partner: partner }) : write(401, { error: 'invalid_token' });
|
|
58
|
+
}
|
|
59
|
+
write(404, {});
|
|
60
|
+
});
|
|
61
|
+
provider.listen(providerPort, '127.0.0.1');
|
|
62
|
+
await once(provider, 'listening');
|
|
63
|
+
const env = { ...process.env, MCP_PORT: String(mcpPort), MCP_HOST: '127.0.0.1',
|
|
64
|
+
MODLENS_HOME: root, MODLENS_CACHE_ROOT: join(root, 'cache'), DATABASE_URL: `file:${join(root, 'oauth.db')}`,
|
|
65
|
+
MODLENS_AUTO_EMBED: '0', MODLENS_AUTO_GRAPH: '0', MODLENS_HOSTED_LIMITS: '1', MODLENS_HOSTED_AUTH: 'oauth',
|
|
66
|
+
MODLENS_HOSTED_PROXY_SECRET: '', MODLENS_HOSTED_MC_SOURCE: '0', MODLENS_OAUTH_PUBLIC_URL: resource,
|
|
67
|
+
MODLENS_OAUTH_ISSUER: issuer, MODLENS_OAUTH_CLIENT_ID: 'test-modlens', MODLENS_OAUTH_SCOPES: 'partner.read offline_access',
|
|
68
|
+
MODLENS_OAUTH_PROFILE_URL: issuer + '/profile', MODLENS_OAUTH_REQUIRED_FIELD: 'is_partner',
|
|
69
|
+
MODLENS_OAUTH_REQUIRED_VALUE: 'true', MODLENS_OAUTH_STORAGE_KEY: randomBytes(32).toString('base64') };
|
|
70
|
+
let server;
|
|
71
|
+
async function start() {
|
|
72
|
+
server = spawn(process.execPath, [entry], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
|
|
73
|
+
let output = '';
|
|
74
|
+
server.stderr.on('data', chunk => { output += chunk; });
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
const timer = setTimeout(() => reject(new Error(`ModLens startup timed out: ${output}`)), 20000);
|
|
77
|
+
const ready = () => { if (output.includes('listening on')) { clearTimeout(timer); server.stderr.off('data', ready); resolve(); } };
|
|
78
|
+
server.stderr.on('data', ready);
|
|
79
|
+
server.once('error', reject);
|
|
80
|
+
server.once('exit', () => { clearTimeout(timer); reject(new Error(`ModLens exited: ${output}`)); });
|
|
81
|
+
ready();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function stop() {
|
|
85
|
+
if (!server || server.exitCode !== null) return;
|
|
86
|
+
const exited = once(server, 'exit');
|
|
87
|
+
server.kill();
|
|
88
|
+
await exited;
|
|
89
|
+
}
|
|
90
|
+
const fetchManual = (url, init) => fetch(url, { ...init, redirect: 'manual' });
|
|
91
|
+
const form = data => new URLSearchParams(data);
|
|
92
|
+
const initialize = (access, extra = {}) => fetchManual(resource, { method: 'POST', headers: {
|
|
93
|
+
Authorization: `Bearer ${access}`, 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', ...extra,
|
|
94
|
+
}, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {
|
|
95
|
+
protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'oauth-test', version: '1' },
|
|
96
|
+
} }) });
|
|
97
|
+
let clientId;
|
|
98
|
+
async function login() {
|
|
99
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
100
|
+
const pkce = createHash('sha256').update(verifier).digest('base64url');
|
|
101
|
+
const callback = 'http://127.0.0.1:54321/callback';
|
|
102
|
+
const auth = new URL(`http://127.0.0.1:${mcpPort}/oauth/authorize`);
|
|
103
|
+
for (const [key, value] of Object.entries({ client_id: clientId, redirect_uri: callback, response_type: 'code',
|
|
104
|
+
code_challenge: pkce, code_challenge_method: 'S256', state: 'client-state', resource, scope: 'modlens' }))
|
|
105
|
+
auth.searchParams.set(key, value);
|
|
106
|
+
const upstream = await fetchManual(auth);
|
|
107
|
+
assert.equal(upstream.status, 302);
|
|
108
|
+
const back = await fetchManual(upstream.headers.get('location'));
|
|
109
|
+
assert.equal(back.status, 302);
|
|
110
|
+
const done = await fetchManual(back.headers.get('location'));
|
|
111
|
+
assert.equal(done.status, 200);
|
|
112
|
+
const approval = (await done.text()).match(/name="approval" value="([^"]+)"/)?.[1];
|
|
113
|
+
assert.ok(approval);
|
|
114
|
+
const approved = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/approve`, { method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form({ approval, decision: 'allow' }) });
|
|
116
|
+
assert.equal(approved.status, 302);
|
|
117
|
+
const result = new URL(approved.headers.get('location'));
|
|
118
|
+
assert.equal(result.searchParams.get('state'), 'client-state');
|
|
119
|
+
assert.equal(result.searchParams.get('iss'), `http://127.0.0.1:${mcpPort}`);
|
|
120
|
+
const tokenBody = form({ grant_type: 'authorization_code', client_id: clientId,
|
|
121
|
+
code: result.searchParams.get('code'), redirect_uri: callback, code_verifier: verifier, resource });
|
|
122
|
+
const exchanged = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/token`, { method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: tokenBody });
|
|
124
|
+
assert.equal(exchanged.status, 200);
|
|
125
|
+
const tokens = await exchanged.json();
|
|
126
|
+
const replay = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/token`, { method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: tokenBody });
|
|
128
|
+
assert.equal(replay.status, 400, 'authorization code cannot be replayed');
|
|
129
|
+
return tokens;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await start();
|
|
133
|
+
const absent = await fetchManual(resource);
|
|
134
|
+
assert.equal(absent.status, 401);
|
|
135
|
+
assert.match(absent.headers.get('www-authenticate'), /resource_metadata=/);
|
|
136
|
+
const metadata = await (await fetchManual(`http://127.0.0.1:${mcpPort}/.well-known/oauth-protected-resource/mcp`)).json();
|
|
137
|
+
assert.deepEqual(metadata.authorization_servers, [`http://127.0.0.1:${mcpPort}`]);
|
|
138
|
+
const registration = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/register`, { method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['http://127.0.0.1:54321/callback'] }) });
|
|
140
|
+
assert.equal(registration.status, 201);
|
|
141
|
+
clientId = (await registration.json()).client_id;
|
|
142
|
+
const tokens = await login();
|
|
143
|
+
assert.match(tokens.access_token, /^mla_/);
|
|
144
|
+
assert.match(tokens.refresh_token, /^mlr_/);
|
|
145
|
+
assert.equal((await initialize(tokens.access_token)).status, 200);
|
|
146
|
+
assert.equal((await initialize(tokens.access_token, { 'x-modlens-user-id': 'forged' })).status, 401);
|
|
147
|
+
await stop();
|
|
148
|
+
await start();
|
|
149
|
+
assert.equal((await initialize(tokens.access_token)).status, 200, 'grant survives restart');
|
|
150
|
+
const refresh = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/token`, { method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form({ grant_type: 'refresh_token',
|
|
152
|
+
client_id: clientId, refresh_token: tokens.refresh_token, resource }) });
|
|
153
|
+
assert.equal(refresh.status, 200);
|
|
154
|
+
const rotated = await refresh.json();
|
|
155
|
+
const replay = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/token`, { method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form({ grant_type: 'refresh_token',
|
|
157
|
+
client_id: clientId, refresh_token: tokens.refresh_token }) });
|
|
158
|
+
assert.equal(replay.status, 400);
|
|
159
|
+
const firstSession = await initialize(rotated.access_token);
|
|
160
|
+
assert.equal(firstSession.status, 200);
|
|
161
|
+
const sessionId = firstSession.headers.get('mcp-session-id');
|
|
162
|
+
assert.ok(sessionId);
|
|
163
|
+
subject = 'customer:99';
|
|
164
|
+
const otherAccount = await login();
|
|
165
|
+
assert.equal((await initialize(otherAccount.access_token, { 'mcp-session-id': sessionId })).status, 404,
|
|
166
|
+
'one account cannot reuse another account session');
|
|
167
|
+
profiles.delete(`upstream-${serial}`);
|
|
168
|
+
assert.equal((await initialize(otherAccount.access_token)).status, 401, 'revoked upstream access fails closed');
|
|
169
|
+
partner = false;
|
|
170
|
+
assert.equal((await initialize(rotated.access_token)).status, 403);
|
|
171
|
+
assert.equal((await initialize(rotated.access_token)).status, 401);
|
|
172
|
+
const denied = await fetchManual(`http://127.0.0.1:${mcpPort}/oauth/authorize?client_id=bad`);
|
|
173
|
+
assert.equal(denied.status, 400);
|
|
174
|
+
await stop();
|
|
175
|
+
delete env.MODLENS_OAUTH_REQUIRED_FIELD;
|
|
176
|
+
delete env.MODLENS_OAUTH_REQUIRED_VALUE;
|
|
177
|
+
await start();
|
|
178
|
+
const generalAccount = await login();
|
|
179
|
+
assert.equal((await initialize(generalAccount.access_token)).status, 200,
|
|
180
|
+
'self-hosted mode accepts a valid account without a claim rule');
|
|
181
|
+
console.log('Hosted OAuth discovery, registration, sign-in, persistence, refresh, replay, session, revocation, partner, gateway-header and self-hosted checks passed.');
|
|
182
|
+
} finally {
|
|
183
|
+
await stop();
|
|
184
|
+
await new Promise(resolve => provider.close(resolve));
|
|
185
|
+
await rm(root, { recursive: true, force: true });
|
|
186
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Real transports, synthetic source only: no Minecraft download or decompilation.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createServer } from "node:net";
|
|
10
|
+
import { once } from "node:events";
|
|
11
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
13
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
14
|
+
import Database from "better-sqlite3";
|
|
15
|
+
|
|
16
|
+
const root = await mkdtemp(join(tmpdir(), "modlens-hosted-test-"));
|
|
17
|
+
const entry = fileURLToPath(new URL("../dist/server.js", import.meta.url));
|
|
18
|
+
const listener = createServer();
|
|
19
|
+
listener.listen(0, "127.0.0.1");
|
|
20
|
+
await once(listener, "listening");
|
|
21
|
+
const port = listener.address().port;
|
|
22
|
+
await new Promise(resolve => listener.close(resolve));
|
|
23
|
+
const endpoint = new URL(`http://127.0.0.1:${port}/mcp`);
|
|
24
|
+
const secret = randomBytes(32).toString("hex");
|
|
25
|
+
const env = { ...process.env, MCP_PORT: String(port), MCP_HOST: "127.0.0.1", MODLENS_HOME: root,
|
|
26
|
+
MODLENS_CACHE_ROOT: join(root, "cache"), DATABASE_URL: `file:${join(root, "usage.db")}`,
|
|
27
|
+
MODLENS_HOSTED_LIMITS: "1", MODLENS_HOSTED_PROXY_SECRET: secret, MODLENS_HOSTED_SOURCE_LINES: "200",
|
|
28
|
+
MODLENS_HOSTED_MC_SOURCE: "1", MODLENS_HOSTED_MC_SOURCE_TEAMS: "partner",
|
|
29
|
+
MODLENS_HOSTED_DAILY_BYTES: "15000", MODLENS_HOSTED_PERIOD_BYTES: "100000",
|
|
30
|
+
MODLENS_HOSTED_DAILY_REQUESTS: "1000", MODLENS_HOSTED_MINUTE_REQUESTS: "120",
|
|
31
|
+
MODLENS_HOSTED_RESPONSE_BYTES: "131072", MODLENS_AUTO_EMBED: "0", MODLENS_AUTO_GRAPH: "0" };
|
|
32
|
+
const source = Array.from({ length: 600 }, (_, i) => `// Synthetic fixture line ${i + 1}`).join("\n");
|
|
33
|
+
const fixtureDir = join(env.MODLENS_CACHE_ROOT, "mc-decompiled-named", "1.21.1", "example");
|
|
34
|
+
await mkdir(fixtureDir, { recursive: true });
|
|
35
|
+
await writeFile(join(fixtureDir, "Fixture.java"), source);
|
|
36
|
+
await writeFile(join(env.MODLENS_CACHE_ROOT, "mc-decompiled-named", "1.21.1", ".decompile.done"), "0");
|
|
37
|
+
const headers = (user, team) => ({ "x-modlens-proxy-secret": secret, "x-modlens-user-id": user,
|
|
38
|
+
...(team ? { "x-modlens-team-id": team } : {}) });
|
|
39
|
+
let server, exited;
|
|
40
|
+
const clients = [];
|
|
41
|
+
async function start() {
|
|
42
|
+
let logs = "";
|
|
43
|
+
server = spawn(process.execPath, [entry], { cwd: root, env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
44
|
+
exited = once(server, "exit");
|
|
45
|
+
server.stderr.on("data", data => { logs += data; });
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
const timer = setTimeout(() => reject(new Error(`Startup timed out: ${logs}`)), 15000);
|
|
48
|
+
const ready = () => {
|
|
49
|
+
if (logs.includes("listening on")) { clearTimeout(timer); server.stderr.off("data", ready); resolve(); }
|
|
50
|
+
};
|
|
51
|
+
server.stderr.on("data", ready);
|
|
52
|
+
server.once("error", reject);
|
|
53
|
+
server.once("exit", () => { clearTimeout(timer); reject(new Error(`Server exited: ${logs}`)); });
|
|
54
|
+
ready();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function connect(user, team) {
|
|
58
|
+
const client = new Client({ name: "hosted-policy-test", version: "1" });
|
|
59
|
+
clients.push(client);
|
|
60
|
+
const transport = new StreamableHTTPClientTransport(endpoint, { requestInit: { headers: headers(user, team) } });
|
|
61
|
+
await client.connect(transport);
|
|
62
|
+
return { client, transport };
|
|
63
|
+
}
|
|
64
|
+
const read = (client, startLine = 1) => client.callTool({ name: "mc_source", arguments: {
|
|
65
|
+
action: "get_source", version: "1.21.1", className: "example.Fixture", startLine, maxLines: 10000,
|
|
66
|
+
} });
|
|
67
|
+
let passed = false;
|
|
68
|
+
try {
|
|
69
|
+
await start();
|
|
70
|
+
const unauthorized = await fetch(endpoint, { method: "POST", headers: { "x-modlens-user-id": "spoof" }, body: "{}" });
|
|
71
|
+
assert.equal(unauthorized.status, 401);
|
|
72
|
+
const publicUser = await connect("public-developer");
|
|
73
|
+
const publicTools = await publicUser.client.listTools();
|
|
74
|
+
const publicMc = publicTools.tools.find(t => t.name === "mc_source");
|
|
75
|
+
assert.ok(publicMc.inputSchema.properties.action.enum.includes("source_info"));
|
|
76
|
+
assert.ok(publicMc.inputSchema.properties.action.enum.includes("index_status"));
|
|
77
|
+
assert.ok(publicMc.inputSchema.properties.action.enum.includes("get_source"));
|
|
78
|
+
assert.ok(!publicMc.inputSchema.properties.action.enum.includes("bytecode"));
|
|
79
|
+
await publicUser.client.callTool({ name: "report_issue", arguments: { action: "help" } });
|
|
80
|
+
// Seed synthetic version metadata so preparation can index the cached fixture offline.
|
|
81
|
+
const fixtureDb = new Database(join(root, "usage.db"));
|
|
82
|
+
fixtureDb.prepare("INSERT INTO mc_versions (version_id, type, release_time) VALUES (?, ?, ?)")
|
|
83
|
+
.run("1.21.1", "release", Date.now());
|
|
84
|
+
const publicSource = await read(publicUser.client);
|
|
85
|
+
assert.ok(!publicSource.isError, JSON.stringify(publicSource));
|
|
86
|
+
assert.ok(!JSON.stringify(publicSource).includes("Synthetic fixture"));
|
|
87
|
+
assert.equal(JSON.parse(publicSource.content[0].text).status, "preparing");
|
|
88
|
+
let prepared;
|
|
89
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
91
|
+
prepared = JSON.parse((await read(publicUser.client)).content[0].text);
|
|
92
|
+
if (prepared.status === "ready") break;
|
|
93
|
+
}
|
|
94
|
+
assert.equal(prepared.status, "ready", JSON.stringify(prepared));
|
|
95
|
+
assert.equal(prepared.indexed, true);
|
|
96
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
97
|
+
if (fixtureDb.prepare("SELECT indexed FROM mc_versions WHERE version_id = ?").get("1.21.1")?.indexed) break;
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
99
|
+
}
|
|
100
|
+
assert.equal(fixtureDb.prepare("SELECT indexed FROM mc_versions WHERE version_id = ?").get("1.21.1")?.indexed, 1,
|
|
101
|
+
"first hosted Minecraft request must index the complete cached version");
|
|
102
|
+
fixtureDb.close();
|
|
103
|
+
const fullStatus = await publicUser.client.callTool({ name: "mc_source", arguments: {
|
|
104
|
+
action: "index_status", version: "1.21.1",
|
|
105
|
+
} });
|
|
106
|
+
assert.deepEqual(JSON.parse(fullStatus.content[0].text), { version: "1.21.1", complete: true, status: "ready" });
|
|
107
|
+
const info = await publicUser.client.callTool({ name: "mc_source", arguments: {
|
|
108
|
+
action: "source_info", version: "1.21.1", className: "example.Fixture",
|
|
109
|
+
} });
|
|
110
|
+
assert.deepEqual(JSON.parse(info.content[0].text), { version: "1.21.1", className: "example/Fixture", cached: true, totalLines: 600 });
|
|
111
|
+
const missingInfo = await publicUser.client.callTool({ name: "mc_source", arguments: {
|
|
112
|
+
action: "source_info", version: "1.21.1", className: "example.Missing",
|
|
113
|
+
} });
|
|
114
|
+
assert.deepEqual(JSON.parse(missingInfo.content[0].text), { version: "1.21.1", className: "example/Missing", cached: false, totalLines: null });
|
|
115
|
+
const publicSearch = await publicUser.client.callTool({ name: "mc_source", arguments: {
|
|
116
|
+
action: "search_code", version: "1.21.1", query: "Synthetic fixture line 319",
|
|
117
|
+
} });
|
|
118
|
+
assert.ok(!publicSearch.isError, JSON.stringify(publicSearch));
|
|
119
|
+
assert.deepEqual(JSON.parse(publicSearch.content[0].text), [{ file: "example/Fixture.java", line: 319 }]);
|
|
120
|
+
const indexedSearch = await publicUser.client.callTool({ name: "mc_source", arguments: {
|
|
121
|
+
action: "search_indexed", version: "1.21.1", query: "fixture",
|
|
122
|
+
} });
|
|
123
|
+
assert.ok(!indexedSearch.isError, JSON.stringify(indexedSearch));
|
|
124
|
+
assert.deepEqual(JSON.parse(indexedSearch.content[0].text), [{ className: "example/Fixture" }]);
|
|
125
|
+
const { client, transport } = await connect("developer-a", "partner");
|
|
126
|
+
const listed = await client.listTools();
|
|
127
|
+
assert.ok(listed.tools.some(t => t.name === "report_issue"), "Hosted users must discover issue reporting");
|
|
128
|
+
const reporter = await connect("issue-reporter");
|
|
129
|
+
const preparedIssue = await reporter.client.callTool({ name: "report_issue", arguments: {
|
|
130
|
+
action: "prepare", title: "Hosted test fixture", summary: "Synthetic report; never submit.",
|
|
131
|
+
} });
|
|
132
|
+
assert.ok(!preparedIssue.isError, JSON.stringify(preparedIssue));
|
|
133
|
+
const issuePlan = JSON.parse(preparedIssue.content[0].text);
|
|
134
|
+
assert.equal(issuePlan.state, "draft_prepared");
|
|
135
|
+
assert.equal(issuePlan.executed, false);
|
|
136
|
+
assert.equal(issuePlan.repository.url, "https://github.com/CreeperHost/modlens-mcp");
|
|
137
|
+
const mc = listed.tools.find(t => t.name === "mc_source");
|
|
138
|
+
assert.ok(mc.inputSchema.properties.action.enum.includes("bytecode"));
|
|
139
|
+
for (const action of ["decompile", "get_paths", "index", "decompile_status"]) {
|
|
140
|
+
assert.ok(!mc.inputSchema.properties.action.enum.includes(action), action);
|
|
141
|
+
}
|
|
142
|
+
assert.ok(listed.tools.some(t => t.name === "mc_data"));
|
|
143
|
+
assert.ok(listed.tools.find(t => t.name === "mod_bytecode").inputSchema.properties.startLine);
|
|
144
|
+
const denied = await client.callTool({ name: "mc_source", arguments: { action: "get_paths", version: "1.21.1" } });
|
|
145
|
+
assert.ok(denied.isError);
|
|
146
|
+
const wrongOwner = await fetch(endpoint, { headers: { ...headers("developer-b"), "mcp-session-id": transport.sessionId, accept: "text/event-stream" } });
|
|
147
|
+
assert.equal(wrongOwner.status, 404);
|
|
148
|
+
const wrongTeam = await fetch(endpoint, { headers: { ...headers("developer-a"), "mcp-session-id": transport.sessionId, accept: "text/event-stream" } });
|
|
149
|
+
assert.equal(wrongTeam.status, 404);
|
|
150
|
+
const first = await read(client);
|
|
151
|
+
assert.ok(!first.isError, JSON.stringify(first));
|
|
152
|
+
assert.equal(first.content[0].text, source.split("\n").slice(0, 200).join("\n"));
|
|
153
|
+
const second = await read(client, 201);
|
|
154
|
+
assert.ok(!second.isError, JSON.stringify(second));
|
|
155
|
+
assert.equal(second.content[0].text, source.split("\n").slice(200, 400).join("\n"));
|
|
156
|
+
const exhausted = await read(client, 401);
|
|
157
|
+
assert.ok(exhausted.isError);
|
|
158
|
+
assert.ok(!JSON.stringify(exhausted).includes("Synthetic fixture"));
|
|
159
|
+
const reconnected = await connect("developer-a", "partner");
|
|
160
|
+
assert.ok((await read(reconnected.client)).isError, "reconnect must retain quota");
|
|
161
|
+
for (const c of clients.splice(0)) await c.close();
|
|
162
|
+
server.kill(); await exited;
|
|
163
|
+
await start();
|
|
164
|
+
const restarted = await connect("developer-a", "partner");
|
|
165
|
+
assert.ok((await read(restarted.client)).isError, "restart must retain quota");
|
|
166
|
+
const different = await connect("developer-b", "partner");
|
|
167
|
+
assert.ok(!(await read(different.client)).isError, "separate verified accounts have separate allowances");
|
|
168
|
+
const local = new Client({ name: "hosted-local-test", version: "1" });
|
|
169
|
+
clients.push(local);
|
|
170
|
+
await local.connect(new StdioClientTransport({ command: process.execPath, args: [entry], cwd: root,
|
|
171
|
+
env: { ...env, MCP_PORT: "" }, stderr: "pipe" }));
|
|
172
|
+
const localResult = await read(local);
|
|
173
|
+
assert.ok(!localResult.isError, JSON.stringify(localResult));
|
|
174
|
+
assert.equal(localResult.content[0].text, source, "local source access remains unrestricted");
|
|
175
|
+
passed = true;
|
|
176
|
+
console.log("PASS: public metadata-only access, team source access, authentication, account/session binding, source paging, cumulative limits, reconnect/restart persistence and unrestricted local access");
|
|
177
|
+
} finally {
|
|
178
|
+
for (const client of clients) await client.close().catch(() => {});
|
|
179
|
+
if (server && server.exitCode === null) { server.kill(); await exited; }
|
|
180
|
+
if (passed) await rm(root, { recursive: true, force: true });
|
|
181
|
+
else console.error(`Fixture retained for debugging: ${root}`);
|
|
182
|
+
}
|