@lightsage/mcp 0.4.16 → 0.4.17
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/README.md +2 -2
- package/api/mcp.js +30 -0
- package/bin/mcp-server.js +1757 -508
- package/bin/mcp-server.js.map +55 -24
- package/esm/funcs/agentAnalyticsCountriesList.d.ts +16 -0
- package/esm/funcs/agentAnalyticsCountriesList.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsCountriesList.js +94 -0
- package/esm/funcs/agentAnalyticsCountriesList.js.map +1 -0
- package/esm/funcs/agentAnalyticsErrorPagesList.d.ts +16 -0
- package/esm/funcs/agentAnalyticsErrorPagesList.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsErrorPagesList.js +96 -0
- package/esm/funcs/agentAnalyticsErrorPagesList.js.map +1 -0
- package/esm/funcs/agentAnalyticsPagesList.d.ts +16 -0
- package/esm/funcs/agentAnalyticsPagesList.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsPagesList.js +98 -0
- package/esm/funcs/agentAnalyticsPagesList.js.map +1 -0
- package/esm/funcs/agentAnalyticsSitesList.d.ts +15 -0
- package/esm/funcs/agentAnalyticsSitesList.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsSitesList.js +72 -0
- package/esm/funcs/agentAnalyticsSitesList.js.map +1 -0
- package/esm/funcs/agentAnalyticsSourcesList.d.ts +16 -0
- package/esm/funcs/agentAnalyticsSourcesList.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsSourcesList.js +93 -0
- package/esm/funcs/agentAnalyticsSourcesList.js.map +1 -0
- package/esm/funcs/agentAnalyticsSummaryRetrieve.d.ts +16 -0
- package/esm/funcs/agentAnalyticsSummaryRetrieve.d.ts.map +1 -0
- package/esm/funcs/agentAnalyticsSummaryRetrieve.js +93 -0
- package/esm/funcs/agentAnalyticsSummaryRetrieve.js.map +1 -0
- package/esm/funcs/tasksCreate.d.ts +16 -0
- package/esm/funcs/tasksCreate.d.ts.map +1 -0
- package/esm/funcs/tasksCreate.js +83 -0
- package/esm/funcs/tasksCreate.js.map +1 -0
- package/esm/landing-page.js +1 -1
- package/esm/lib/config.d.ts +2 -2
- package/esm/lib/config.js +2 -2
- package/esm/mcp-server/mcp-server.js +1 -1
- package/esm/mcp-server/server.d.ts.map +1 -1
- package/esm/mcp-server/server.js +15 -1
- package/esm/mcp-server/server.js.map +1 -1
- package/esm/mcp-server/tools/agentAnalyticsCountriesList.d.ts +7 -0
- package/esm/mcp-server/tools/agentAnalyticsCountriesList.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsCountriesList.js +35 -0
- package/esm/mcp-server/tools/agentAnalyticsCountriesList.js.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsErrorPagesList.d.ts +7 -0
- package/esm/mcp-server/tools/agentAnalyticsErrorPagesList.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsErrorPagesList.js +35 -0
- package/esm/mcp-server/tools/agentAnalyticsErrorPagesList.js.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsPagesList.d.ts +7 -0
- package/esm/mcp-server/tools/agentAnalyticsPagesList.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsPagesList.js +35 -0
- package/esm/mcp-server/tools/agentAnalyticsPagesList.js.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSitesList.d.ts +3 -0
- package/esm/mcp-server/tools/agentAnalyticsSitesList.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSitesList.js +30 -0
- package/esm/mcp-server/tools/agentAnalyticsSitesList.js.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSourcesList.d.ts +7 -0
- package/esm/mcp-server/tools/agentAnalyticsSourcesList.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSourcesList.js +35 -0
- package/esm/mcp-server/tools/agentAnalyticsSourcesList.js.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSummaryRetrieve.d.ts +7 -0
- package/esm/mcp-server/tools/agentAnalyticsSummaryRetrieve.d.ts.map +1 -0
- package/esm/mcp-server/tools/agentAnalyticsSummaryRetrieve.js +35 -0
- package/esm/mcp-server/tools/agentAnalyticsSummaryRetrieve.js.map +1 -0
- package/esm/mcp-server/tools/tasksCreate.d.ts +7 -0
- package/esm/mcp-server/tools/tasksCreate.d.ts.map +1 -0
- package/esm/mcp-server/tools/tasksCreate.js +35 -0
- package/esm/mcp-server/tools/tasksCreate.js.map +1 -0
- package/esm/models/createtaskrequest.d.ts +65 -0
- package/esm/models/createtaskrequest.d.ts.map +1 -0
- package/esm/models/createtaskrequest.js +64 -0
- package/esm/models/createtaskrequest.js.map +1 -0
- package/esm/models/graderdefinition.d.ts +1 -0
- package/esm/models/graderdefinition.d.ts.map +1 -1
- package/esm/models/graderdefinition.js +1 -0
- package/esm/models/graderdefinition.js.map +1 -1
- package/esm/models/publicagentanalyticscountrylistresponse.d.ts +10 -0
- package/esm/models/publicagentanalyticscountrylistresponse.d.ts.map +1 -0
- package/esm/models/publicagentanalyticscountrylistresponse.js +13 -0
- package/esm/models/publicagentanalyticscountrylistresponse.js.map +1 -0
- package/esm/models/publicagentanalyticserrorpagelistresponse.d.ts +11 -0
- package/esm/models/publicagentanalyticserrorpagelistresponse.d.ts.map +1 -0
- package/esm/models/publicagentanalyticserrorpagelistresponse.js +14 -0
- package/esm/models/publicagentanalyticserrorpagelistresponse.js.map +1 -0
- package/esm/models/publicagentanalyticspagelistresponse.d.ts +11 -0
- package/esm/models/publicagentanalyticspagelistresponse.d.ts.map +1 -0
- package/esm/models/publicagentanalyticspagelistresponse.js +14 -0
- package/esm/models/publicagentanalyticspagelistresponse.js.map +1 -0
- package/esm/models/publicagentanalyticssite.d.ts +9 -0
- package/esm/models/publicagentanalyticssite.d.ts.map +1 -0
- package/esm/models/publicagentanalyticssite.js +12 -0
- package/esm/models/publicagentanalyticssite.js.map +1 -0
- package/esm/models/publicagentanalyticssitelistresponse.d.ts +7 -0
- package/esm/models/publicagentanalyticssitelistresponse.d.ts.map +1 -0
- package/esm/models/publicagentanalyticssitelistresponse.js +10 -0
- package/esm/models/publicagentanalyticssitelistresponse.js.map +1 -0
- package/esm/models/publicagentanalyticssourcelistresponse.d.ts +10 -0
- package/esm/models/publicagentanalyticssourcelistresponse.d.ts.map +1 -0
- package/esm/models/publicagentanalyticssourcelistresponse.js +13 -0
- package/esm/models/publicagentanalyticssourcelistresponse.js.map +1 -0
- package/esm/models/publicagentanalyticssummary.d.ts +11 -0
- package/esm/models/publicagentanalyticssummary.d.ts.map +1 -0
- package/esm/models/publicagentanalyticssummary.js +14 -0
- package/esm/models/publicagentanalyticssummary.js.map +1 -0
- package/esm/models/publicagentcountry.d.ts +7 -0
- package/esm/models/publicagentcountry.d.ts.map +1 -0
- package/esm/models/publicagentcountry.js +11 -0
- package/esm/models/publicagentcountry.js.map +1 -0
- package/esm/models/publicagenterrorpage.d.ts +12 -0
- package/esm/models/publicagenterrorpage.d.ts.map +1 -0
- package/esm/models/publicagenterrorpage.js +16 -0
- package/esm/models/publicagenterrorpage.js.map +1 -0
- package/esm/models/publicagenterrorsource.d.ts +40 -0
- package/esm/models/publicagenterrorsource.d.ts.map +1 -0
- package/esm/models/publicagenterrorsource.js +40 -0
- package/esm/models/publicagenterrorsource.js.map +1 -0
- package/esm/models/publicagenttrafficpage.d.ts +14 -0
- package/esm/models/publicagenttrafficpage.d.ts.map +1 -0
- package/esm/models/publicagenttrafficpage.js +17 -0
- package/esm/models/publicagenttrafficpage.js.map +1 -0
- package/esm/models/publicagenttrafficsource.d.ts +47 -0
- package/esm/models/publicagenttrafficsource.d.ts.map +1 -0
- package/esm/models/publicagenttrafficsource.js +44 -0
- package/esm/models/publicagenttrafficsource.js.map +1 -0
- package/esm/models/publicagenttraffictotals.d.ts +13 -0
- package/esm/models/publicagenttraffictotals.d.ts.map +1 -0
- package/esm/models/publicagenttraffictotals.js +16 -0
- package/esm/models/publicagenttraffictotals.js.map +1 -0
- package/esm/models/updatetaskrequest.d.ts +8 -8
- package/esm/models/updatetaskrequest.d.ts.map +1 -1
- package/esm/models/updatetaskrequest.js +6 -6
- package/esm/models/updatetaskrequest.js.map +1 -1
- package/esm/models/v2agentanalyticscountrieslistop.d.ts +13 -0
- package/esm/models/v2agentanalyticscountrieslistop.d.ts.map +1 -0
- package/esm/models/v2agentanalyticscountrieslistop.js +18 -0
- package/esm/models/v2agentanalyticscountrieslistop.js.map +1 -0
- package/esm/models/v2agentanalyticserrorpageslistop.d.ts +39 -0
- package/esm/models/v2agentanalyticserrorpageslistop.d.ts.map +1 -0
- package/esm/models/v2agentanalyticserrorpageslistop.js +39 -0
- package/esm/models/v2agentanalyticserrorpageslistop.js.map +1 -0
- package/esm/models/v2agentanalyticspageslistop.d.ts +68 -0
- package/esm/models/v2agentanalyticspageslistop.d.ts.map +1 -0
- package/esm/models/v2agentanalyticspageslistop.js +65 -0
- package/esm/models/v2agentanalyticspageslistop.js.map +1 -0
- package/esm/models/v2agentanalyticssourceslistop.d.ts +12 -0
- package/esm/models/v2agentanalyticssourceslistop.d.ts.map +1 -0
- package/esm/models/v2agentanalyticssourceslistop.js +17 -0
- package/esm/models/v2agentanalyticssourceslistop.js.map +1 -0
- package/esm/models/v2agentanalyticssummaryretrieveop.d.ts +12 -0
- package/esm/models/v2agentanalyticssummaryretrieveop.d.ts.map +1 -0
- package/esm/models/v2agentanalyticssummaryretrieveop.js +17 -0
- package/esm/models/v2agentanalyticssummaryretrieveop.js.map +1 -0
- package/esm/models/v2inlinemcpdefinition.js +1 -1
- package/esm/models/v2inlinemcpdefinition.js.map +1 -1
- package/esm/models/v2repository.d.ts +4 -4
- package/esm/models/v2repository.d.ts.map +1 -1
- package/esm/models/v2repository.js +3 -3
- package/esm/models/v2repository.js.map +1 -1
- package/esm/models/v2taskscreateop.d.ts +13 -0
- package/esm/models/v2taskscreateop.d.ts.map +1 -0
- package/esm/models/v2taskscreateop.js +16 -0
- package/esm/models/v2taskscreateop.js.map +1 -0
- package/esm/tool-names.d.ts.map +1 -1
- package/esm/tool-names.js +28 -0
- package/esm/tool-names.js.map +1 -1
- package/lib/tracker.js +219 -0
- package/package.json +3 -1
- package/src/funcs/agentAnalyticsCountriesList.ts +159 -0
- package/src/funcs/agentAnalyticsErrorPagesList.ts +161 -0
- package/src/funcs/agentAnalyticsPagesList.ts +163 -0
- package/src/funcs/agentAnalyticsSitesList.ts +124 -0
- package/src/funcs/agentAnalyticsSourcesList.ts +158 -0
- package/src/funcs/agentAnalyticsSummaryRetrieve.ts +158 -0
- package/src/funcs/tasksCreate.ts +145 -0
- package/src/landing-page.ts +1 -1
- package/src/lib/config.ts +2 -2
- package/src/mcp-server/mcp-server.ts +1 -1
- package/src/mcp-server/server.ts +15 -1
- package/src/mcp-server/tools/agentAnalyticsCountriesList.ts +43 -0
- package/src/mcp-server/tools/agentAnalyticsErrorPagesList.ts +43 -0
- package/src/mcp-server/tools/agentAnalyticsPagesList.ts +43 -0
- package/src/mcp-server/tools/agentAnalyticsSitesList.ts +36 -0
- package/src/mcp-server/tools/agentAnalyticsSourcesList.ts +43 -0
- package/src/mcp-server/tools/agentAnalyticsSummaryRetrieve.ts +43 -0
- package/src/mcp-server/tools/tasksCreate.ts +43 -0
- package/src/models/createtaskrequest.ts +98 -0
- package/src/models/graderdefinition.ts +2 -0
- package/src/models/publicagentanalyticscountrylistresponse.ts +28 -0
- package/src/models/publicagentanalyticserrorpagelistresponse.ts +32 -0
- package/src/models/publicagentanalyticspagelistresponse.ts +30 -0
- package/src/models/publicagentanalyticssite.ts +26 -0
- package/src/models/publicagentanalyticssitelistresponse.ts +20 -0
- package/src/models/publicagentanalyticssourcelistresponse.ts +28 -0
- package/src/models/publicagentanalyticssummary.ts +28 -0
- package/src/models/publicagentcountry.ts +16 -0
- package/src/models/publicagenterrorpage.ts +35 -0
- package/src/models/publicagenterrorsource.ts +63 -0
- package/src/models/publicagenttrafficpage.ts +46 -0
- package/src/models/publicagenttrafficsource.ts +77 -0
- package/src/models/publicagenttraffictotals.ts +42 -0
- package/src/models/updatetaskrequest.ts +14 -10
- package/src/models/v2agentanalyticscountrieslistop.ts +45 -0
- package/src/models/v2agentanalyticserrorpageslistop.ts +79 -0
- package/src/models/v2agentanalyticspageslistop.ts +119 -0
- package/src/models/v2agentanalyticssourceslistop.ts +43 -0
- package/src/models/v2agentanalyticssummaryretrieveop.ts +43 -0
- package/src/models/v2inlinemcpdefinition.ts +1 -1
- package/src/models/v2repository.ts +5 -5
- package/src/models/v2taskscreateop.ts +34 -0
- package/src/tool-names.ts +28 -0
- package/test/tracker.test.js +323 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
process.env.MCP_OAUTH_ENABLED = "false";
|
|
6
|
+
process.env.LIGHTSAGE_MCP_TRACKER_API_KEY = "lsmcp_test_key";
|
|
7
|
+
|
|
8
|
+
// Capture analytics batches instead of sending them.
|
|
9
|
+
const batches = [];
|
|
10
|
+
const authChecks = [];
|
|
11
|
+
let analyticsHangs = false;
|
|
12
|
+
const realFetch = globalThis.fetch;
|
|
13
|
+
globalThis.fetch = async (url, init) => {
|
|
14
|
+
if (String(url).includes("/v2/auth/status")) {
|
|
15
|
+
authChecks.push(init.headers["X-Lightsage-Api-Key"]);
|
|
16
|
+
const valid = !String(init.headers["X-Lightsage-Api-Key"]).startsWith("ls_bad");
|
|
17
|
+
return new Response("{}", { status: valid ? 200 : 401 });
|
|
18
|
+
}
|
|
19
|
+
if (String(url).includes("mcp-analytics")) {
|
|
20
|
+
// Never settles and ignores the abort signal.
|
|
21
|
+
if (analyticsHangs) return new Promise(() => {});
|
|
22
|
+
batches.push({ headers: init.headers, body: JSON.parse(init.body) });
|
|
23
|
+
return new Response("{}", { status: 200 });
|
|
24
|
+
}
|
|
25
|
+
return realFetch(url, init);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const { default: handler } = await import("../api/mcp.js");
|
|
29
|
+
const { Client } = await import(
|
|
30
|
+
"@modelcontextprotocol/sdk/client/index.js"
|
|
31
|
+
);
|
|
32
|
+
const { StreamableHTTPClientTransport } = await import(
|
|
33
|
+
"@modelcontextprotocol/sdk/client/streamableHttp.js"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function createServer() {
|
|
37
|
+
return http.createServer(async (req, res) => {
|
|
38
|
+
res.status = (code) => {
|
|
39
|
+
res.statusCode = code;
|
|
40
|
+
return res;
|
|
41
|
+
};
|
|
42
|
+
res.json = (value) => {
|
|
43
|
+
res.setHeader("content-type", "application/json");
|
|
44
|
+
res.end(JSON.stringify(value));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const chunks = [];
|
|
48
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
49
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
50
|
+
req.body = body ? JSON.parse(body) : undefined;
|
|
51
|
+
await handler(req, res);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function initializeRequest(url) {
|
|
56
|
+
return realFetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
accept: "application/json, text/event-stream",
|
|
60
|
+
"content-type": "application/json",
|
|
61
|
+
"lightsage-api-key-auth": "ls_test_key",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
jsonrpc: "2.0",
|
|
65
|
+
id: 1,
|
|
66
|
+
method: "initialize",
|
|
67
|
+
params: {
|
|
68
|
+
protocolVersion: "2025-11-25",
|
|
69
|
+
capabilities: {},
|
|
70
|
+
clientInfo: { name: "tracker-test", version: "1.0.0" },
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sentEvents() {
|
|
77
|
+
return batches.flatMap((batch) =>
|
|
78
|
+
batch.body.events ?? [batch.body]
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test("tracks the handshake and tool calls and flushes before the handler returns", async () => {
|
|
83
|
+
const server = createServer();
|
|
84
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
85
|
+
const address = server.address();
|
|
86
|
+
const url = new URL(`http://127.0.0.1:${address.port}/mcp`);
|
|
87
|
+
const client = new Client({ name: "tracker-test", version: "1.0.0" });
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await client.connect(
|
|
91
|
+
new StreamableHTTPClientTransport(url, {
|
|
92
|
+
fetch: realFetch,
|
|
93
|
+
requestInit: { headers: { "lightsage-api-key-auth": "ls_test_key" } },
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
await client.listTools();
|
|
97
|
+
await client.callTool({ name: "list_tools", arguments: {} });
|
|
98
|
+
|
|
99
|
+
const events = sentEvents();
|
|
100
|
+
const initialize = events.find((e) => e.event === "$mcp_initialize");
|
|
101
|
+
assert.equal(initialize?.client_name, "tracker-test");
|
|
102
|
+
assert.equal(initialize?.server_name, "LightsageMcp");
|
|
103
|
+
assert.ok(events.some((e) => e.event === "$mcp_tools_list"));
|
|
104
|
+
assert.ok(
|
|
105
|
+
events.some((e) =>
|
|
106
|
+
e.event === "$mcp_tool_call" && e.tool_name === "list_tools"
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
assert.ok(
|
|
110
|
+
JSON.stringify(batches[0].headers).includes("lsmcp_test_key"),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Each request is a new stateless server, so these only line up because
|
|
114
|
+
// the session comes from the caller's credential.
|
|
115
|
+
const toolCall = events.find((e) => e.event === "$mcp_tool_call");
|
|
116
|
+
assert.match(toolCall.session_id, /^mcp_caller_[0-9a-f]{32}$/);
|
|
117
|
+
assert.ok(events.every((e) => e.session_id === toolCall.session_id));
|
|
118
|
+
assert.equal(toolCall.client_name, "tracker-test");
|
|
119
|
+
assert.ok(!JSON.stringify(events).includes("ls_test_key"));
|
|
120
|
+
} finally {
|
|
121
|
+
await client.close();
|
|
122
|
+
await new Promise((resolve) => server.close(resolve));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("caller sessions are stable per credential and client, and never contain the credential", async () => {
|
|
127
|
+
const { callerSessionId } = await import("../lib/tracker.js");
|
|
128
|
+
const alice = callerSessionId({
|
|
129
|
+
headers: { "x-lightsage-api-key": "ls_alice", "user-agent": "claude-code/2.1.3" },
|
|
130
|
+
});
|
|
131
|
+
assert.equal(
|
|
132
|
+
alice,
|
|
133
|
+
callerSessionId({
|
|
134
|
+
headers: { "x-lightsage-api-key": "ls_alice", "user-agent": "claude-code/2.1.3" },
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
assert.notEqual(
|
|
138
|
+
alice,
|
|
139
|
+
callerSessionId({ headers: { "x-lightsage-api-key": "ls_bob", "user-agent": "claude-code/2.1.3" } }),
|
|
140
|
+
);
|
|
141
|
+
assert.notEqual(
|
|
142
|
+
alice,
|
|
143
|
+
callerSessionId({ headers: { "x-lightsage-api-key": "ls_alice", "user-agent": "cursor/1.0" } }),
|
|
144
|
+
);
|
|
145
|
+
assert.ok(!alice.includes("ls_alice"));
|
|
146
|
+
assert.match(
|
|
147
|
+
callerSessionId({ headers: { authorization: "Bearer oauth_token" } }),
|
|
148
|
+
/^mcp_caller_/,
|
|
149
|
+
);
|
|
150
|
+
assert.equal(callerSessionId({ headers: {} }), undefined);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a stalled analytics request does not hold the handler open", async () => {
|
|
154
|
+
analyticsHangs = true;
|
|
155
|
+
let handlerMs;
|
|
156
|
+
const server = http.createServer(async (req, res) => {
|
|
157
|
+
const chunks = [];
|
|
158
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
159
|
+
req.body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
160
|
+
const started = Date.now();
|
|
161
|
+
await handler(req, res);
|
|
162
|
+
handlerMs = Date.now() - started;
|
|
163
|
+
});
|
|
164
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
165
|
+
const address = server.address();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const response = await initializeRequest(
|
|
169
|
+
`http://127.0.0.1:${address.port}/mcp`,
|
|
170
|
+
);
|
|
171
|
+
assert.equal(response.status, 200);
|
|
172
|
+
await response.text();
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 3500));
|
|
174
|
+
assert.ok(handlerMs !== undefined && handlerMs < 3500, `handler took ${handlerMs}ms`);
|
|
175
|
+
} finally {
|
|
176
|
+
analyticsHangs = false;
|
|
177
|
+
await new Promise((resolve) => server.close(resolve));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("on Vercel the analytics flush is registered with waitUntil", async () => {
|
|
182
|
+
// Vercel exposes the request context on this symbol; @vercel/functions reads it.
|
|
183
|
+
const contextSymbol = Symbol.for("@vercel/request-context");
|
|
184
|
+
const registered = [];
|
|
185
|
+
globalThis[contextSymbol] = {
|
|
186
|
+
get: () => ({ waitUntil: (promise) => registered.push(promise) }),
|
|
187
|
+
};
|
|
188
|
+
const server = createServer();
|
|
189
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
190
|
+
const address = server.address();
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const response = await initializeRequest(
|
|
194
|
+
`http://127.0.0.1:${address.port}/mcp`,
|
|
195
|
+
);
|
|
196
|
+
assert.equal(response.status, 200);
|
|
197
|
+
await response.text();
|
|
198
|
+
await Promise.all(registered);
|
|
199
|
+
assert.equal(registered.length, 1);
|
|
200
|
+
} finally {
|
|
201
|
+
delete globalThis[contextSymbol];
|
|
202
|
+
await new Promise((resolve) => server.close(resolve));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async function postJsonRpc(url, headers, method, params, id = 1) {
|
|
208
|
+
const response = await realFetch(url, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
accept: "application/json, text/event-stream",
|
|
212
|
+
"content-type": "application/json",
|
|
213
|
+
...headers,
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
|
|
216
|
+
});
|
|
217
|
+
await response.text();
|
|
218
|
+
return response.status;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function withServer(fn) {
|
|
222
|
+
const server = createServer();
|
|
223
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
224
|
+
try {
|
|
225
|
+
await fn(`http://127.0.0.1:${server.address().port}/mcp`);
|
|
226
|
+
} finally {
|
|
227
|
+
await new Promise((resolve) => server.close(resolve));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test("calls with a made-up API key are served but not recorded, even with a forged caller header", async () => {
|
|
232
|
+
await withServer(async (url) => {
|
|
233
|
+
const before = sentEvents().length;
|
|
234
|
+
const status = await postJsonRpc(
|
|
235
|
+
url,
|
|
236
|
+
{
|
|
237
|
+
"x-lightsage-api-key": "ls_bad_made_up",
|
|
238
|
+
"x-lightsage-verified-caller": "oauth:someone_else",
|
|
239
|
+
},
|
|
240
|
+
"tools/call",
|
|
241
|
+
{ name: "list_tools", arguments: {} },
|
|
242
|
+
);
|
|
243
|
+
assert.equal(status, 200);
|
|
244
|
+
assert.equal(sentEvents().length, before);
|
|
245
|
+
assert.ok(authChecks.includes("ls_bad_made_up"));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("the in-app agent's headers attribute the user, run and request, and search results are kept", async () => {
|
|
250
|
+
await withServer(async (url) => {
|
|
251
|
+
const before = sentEvents().length;
|
|
252
|
+
await postJsonRpc(
|
|
253
|
+
url,
|
|
254
|
+
{
|
|
255
|
+
"x-lightsage-api-key": "ls_agent_key",
|
|
256
|
+
"user-agent": "Lightsage-Agent/1.0",
|
|
257
|
+
"x-lightsage-agent-user": "user_123",
|
|
258
|
+
"x-lightsage-agent-run": "run_9",
|
|
259
|
+
"x-lightsage-agent-intent": encodeURIComponent("What's my visibility — today?"),
|
|
260
|
+
},
|
|
261
|
+
"tools/call",
|
|
262
|
+
{ name: "list_tools", arguments: { search_terms: ["visibility"] } },
|
|
263
|
+
);
|
|
264
|
+
const call = sentEvents().slice(before).find((e) => e.event === "$mcp_tool_call");
|
|
265
|
+
assert.equal(call.client_name, "Lightsage-Agent");
|
|
266
|
+
assert.equal(call.distinct_id, "user_123");
|
|
267
|
+
assert.equal(call.conversation_id, "run_9");
|
|
268
|
+
assert.equal(call.intent, "What's my visibility — today?");
|
|
269
|
+
assert.deepEqual(call.properties, { auth: "api_key" });
|
|
270
|
+
// A catalog search result is kept: it is how empty searches are detected
|
|
271
|
+
assert.ok(call.response?.content?.length);
|
|
272
|
+
assert.match(call.session_id, /^mcp_caller_/);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("an API key is checked once and then cached", async () => {
|
|
277
|
+
const { isTrackedApiKey } = await import("../lib/tracker.js");
|
|
278
|
+
let checks = 0;
|
|
279
|
+
const fakeFetch = async () => {
|
|
280
|
+
checks += 1;
|
|
281
|
+
return new Response("{}", { status: 200 });
|
|
282
|
+
};
|
|
283
|
+
assert.equal(await isTrackedApiKey("ls_cached", "https://api.example.test", fakeFetch), true);
|
|
284
|
+
assert.equal(await isTrackedApiKey("ls_cached", "https://api.example.test", fakeFetch), true);
|
|
285
|
+
assert.equal(checks, 1);
|
|
286
|
+
|
|
287
|
+
const failing = async () => {
|
|
288
|
+
throw new Error("network down");
|
|
289
|
+
};
|
|
290
|
+
// An unreachable API does not lose analytics, and is not cached
|
|
291
|
+
assert.equal(await isTrackedApiKey("ls_unknown", "https://api.example.test", failing), true);
|
|
292
|
+
const rejecting = async () => new Response("{}", { status: 401 });
|
|
293
|
+
assert.equal(await isTrackedApiKey("ls_unknown", "https://api.example.test", rejecting), false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("OAuth sessions follow the user, not the token", async () => {
|
|
297
|
+
const { callerSessionId } = await import("../lib/tracker.js");
|
|
298
|
+
const first = callerSessionId({
|
|
299
|
+
headers: {
|
|
300
|
+
authorization: "Bearer token_a",
|
|
301
|
+
"x-lightsage-verified-caller": "oauth:user_1",
|
|
302
|
+
"user-agent": "claude-code/2.1.3",
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const refreshed = callerSessionId({
|
|
306
|
+
headers: {
|
|
307
|
+
authorization: "Bearer token_b",
|
|
308
|
+
"x-lightsage-verified-caller": "oauth:user_1",
|
|
309
|
+
"user-agent": "claude-code/2.1.3",
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
assert.equal(first, refreshed);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
test("only failed calls and catalog tool results are stored", async () => {
|
|
317
|
+
const { shouldStoreResponse } = await import("../lib/tracker.js");
|
|
318
|
+
assert.equal(shouldStoreResponse({ toolName: "list_tools", isError: false }), true);
|
|
319
|
+
assert.equal(shouldStoreResponse({ toolName: "describe_tool_input", isError: false }), true);
|
|
320
|
+
assert.equal(shouldStoreResponse({ toolName: "execute_tool", isError: false }), false);
|
|
321
|
+
assert.equal(shouldStoreResponse({ toolName: "get_profile", isError: false }), false);
|
|
322
|
+
assert.equal(shouldStoreResponse({ toolName: "execute_tool", isError: true }), true);
|
|
323
|
+
});
|