@kryptosai/mcp-observatory 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -27
- package/dist/src/adapters/http.d.ts +2 -2
- package/dist/src/adapters/http.js +12 -3
- package/dist/src/adapters/http.js.map +1 -1
- package/dist/src/adapters/local-process.d.ts +7 -1
- package/dist/src/adapters/local-process.js +9 -3
- package/dist/src/adapters/local-process.js.map +1 -1
- package/dist/src/cassette.d.ts +22 -0
- package/dist/src/cassette.js +28 -0
- package/dist/src/cassette.js.map +1 -0
- package/dist/src/checks/tools-invoke.js +9 -0
- package/dist/src/checks/tools-invoke.js.map +1 -1
- package/dist/src/cli.js +360 -186
- package/dist/src/cli.js.map +1 -1
- package/dist/src/diff.js +39 -0
- package/dist/src/diff.js.map +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/reporters/terminal.js +8 -2
- package/dist/src/reporters/terminal.js.map +1 -1
- package/dist/src/runner.d.ts +16 -0
- package/dist/src/runner.js +28 -5
- package/dist/src/runner.js.map +1 -1
- package/dist/src/transport/recording-transport.d.ts +22 -0
- package/dist/src/transport/recording-transport.js +91 -0
- package/dist/src/transport/recording-transport.js.map +1 -0
- package/dist/src/transport/replay-transport.d.ts +27 -0
- package/dist/src/transport/replay-transport.js +64 -0
- package/dist/src/transport/replay-transport.js.map +1 -0
- package/dist/src/types.d.ts +8 -0
- package/dist/src/verify.d.ts +21 -0
- package/dist/src/verify.js +98 -0
- package/dist/src/verify.js.map +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -32,10 +32,16 @@ Scan every MCP server in your Claude config:
|
|
|
32
32
|
npx @kryptosai/mcp-observatory
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Go deeper — also invoke safe tools to verify they actually run:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx @kryptosai/mcp-observatory
|
|
38
|
+
npx @kryptosai/mcp-observatory scan deep
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Test a specific server:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @kryptosai/mcp-observatory test npx -y @modelcontextprotocol/server-everything
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
Add it to Claude Code as an MCP server:
|
|
@@ -57,6 +63,20 @@ Or add it manually to your config:
|
|
|
57
63
|
}
|
|
58
64
|
```
|
|
59
65
|
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
| Command | What it does |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `scan` | Auto-discover servers from config files and check them all (default) |
|
|
71
|
+
| `scan deep` | Scan and also invoke safe tools to verify they execute |
|
|
72
|
+
| `test <cmd>` | Test a specific server by command |
|
|
73
|
+
| `record <cmd>` | Record a server session to a cassette file for offline replay |
|
|
74
|
+
| `replay <cassette>` | Replay a cassette offline — no live server needed |
|
|
75
|
+
| `verify <cassette> <cmd>` | Verify a live server still matches a recorded cassette |
|
|
76
|
+
| `diff <base> <head>` | Compare two run artifacts for regressions and schema drift |
|
|
77
|
+
| `watch <config>` | Watch a server for changes, alert on regressions |
|
|
78
|
+
| `serve` | Start as an MCP server for AI agents |
|
|
79
|
+
|
|
60
80
|
## What It Does
|
|
61
81
|
|
|
62
82
|
**Check capabilities** — connects to a server and verifies tools, prompts, and resources respond correctly.
|
|
@@ -64,33 +84,35 @@ Or add it manually to your config:
|
|
|
64
84
|
**Invoke tools** — goes beyond listing. Actually calls safe tools (no required params / readOnlyHint) and reports which ones work and which ones crash.
|
|
65
85
|
|
|
66
86
|
```bash
|
|
67
|
-
npx @kryptosai/mcp-observatory scan
|
|
87
|
+
npx @kryptosai/mcp-observatory scan deep
|
|
68
88
|
```
|
|
69
89
|
|
|
70
90
|
**Detect schema drift** — diffs two runs and surfaces added/removed fields, type changes, and breaking parameter changes.
|
|
71
91
|
|
|
72
92
|
```bash
|
|
73
|
-
npx @kryptosai/mcp-observatory diff
|
|
93
|
+
npx @kryptosai/mcp-observatory diff run-a.json run-b.json
|
|
74
94
|
```
|
|
75
95
|
|
|
76
96
|
**Recommend servers** — scans your project for languages, frameworks, databases, and cloud providers, then cross-references the [MCP registry](https://registry.modelcontextprotocol.io) to suggest servers you're missing. Ask your agent "what MCP servers should I add?" and it figures out the rest.
|
|
77
97
|
|
|
78
|
-
**
|
|
98
|
+
**Record / replay / verify** — capture a live session, replay it offline in CI, and verify nothing changed. Like [VCR](https://github.com/vcr/vcr) for MCP.
|
|
79
99
|
|
|
80
100
|
```bash
|
|
81
|
-
|
|
101
|
+
# Record a session
|
|
102
|
+
npx @kryptosai/mcp-observatory record npx -y @modelcontextprotocol/server-everything
|
|
103
|
+
|
|
104
|
+
# Replay offline (no server needed)
|
|
105
|
+
npx @kryptosai/mcp-observatory replay .mcp-observatory/cassettes/latest.cassette.json
|
|
106
|
+
|
|
107
|
+
# Verify the live server still matches
|
|
108
|
+
npx @kryptosai/mcp-observatory verify cassette.json npx -y @modelcontextprotocol/server-everything
|
|
82
109
|
```
|
|
83
110
|
|
|
84
|
-
|
|
111
|
+
**Watch for regressions** — re-runs checks on an interval and alerts when something changes.
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| `run` | Check one server and save a run artifact |
|
|
90
|
-
| `check` | Run a single capability check (tools, prompts, resources, tools-invoke) |
|
|
91
|
-
| `diff` | Compare two runs — regressions, recoveries, schema drift |
|
|
92
|
-
| `report` | Render a run as terminal, JSON, markdown, or HTML |
|
|
93
|
-
| `serve` | Run as an MCP server — exposes scan, check, diff, suggest as tools |
|
|
113
|
+
```bash
|
|
114
|
+
npx @kryptosai/mcp-observatory watch target.json
|
|
115
|
+
```
|
|
94
116
|
|
|
95
117
|
### Scan locations
|
|
96
118
|
|
|
@@ -129,18 +151,6 @@ Works with any MCP server that uses standard transports:
|
|
|
129
151
|
|
|
130
152
|
Servers needing API keys work via `env` in the target config. Python servers work via `uvx`. See the [full compatibility matrix](./docs/compatibility.md) for tested servers and known issues.
|
|
131
153
|
|
|
132
|
-
### HTTP / SSE targets
|
|
133
|
-
|
|
134
|
-
```json
|
|
135
|
-
{
|
|
136
|
-
"targetId": "my-remote-server",
|
|
137
|
-
"adapter": "http",
|
|
138
|
-
"url": "http://localhost:3000/mcp",
|
|
139
|
-
"authToken": "optional-bearer-token",
|
|
140
|
-
"timeoutMs": 15000
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
154
|
### Target config files
|
|
145
155
|
|
|
146
156
|
For more control (env vars, metadata, custom timeout):
|
|
@@ -159,6 +169,46 @@ For more control (env vars, metadata, custom timeout):
|
|
|
159
169
|
npx @kryptosai/mcp-observatory run --target ./target.json
|
|
160
170
|
```
|
|
161
171
|
|
|
172
|
+
### HTTP / SSE targets
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"targetId": "my-remote-server",
|
|
177
|
+
"adapter": "http",
|
|
178
|
+
"url": "http://localhost:3000/mcp",
|
|
179
|
+
"authToken": "optional-bearer-token",
|
|
180
|
+
"timeoutMs": 15000
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## How It Compares
|
|
185
|
+
|
|
186
|
+
| Feature | Observatory | [mcp-recorder](https://github.com/punkpeye/mcp-recorder) | [MCPBench](https://github.com/QuantGeekDev/mcpbench) | [mcp-jest](https://github.com/nicobailon/mcp-jest) |
|
|
187
|
+
|---------|:-----------:|:----------:|:-------:|:-------:|
|
|
188
|
+
| Auto-discover servers | ✅ | — | — | — |
|
|
189
|
+
| Check capabilities | ✅ | — | ✅ | ✅ |
|
|
190
|
+
| Invoke tools | ✅ | — | — | ✅ |
|
|
191
|
+
| Schema drift detection | ✅ | — | — | — |
|
|
192
|
+
| Record / replay | ✅ | ✅ | — | — |
|
|
193
|
+
| Verify against cassette | ✅ | — | — | — |
|
|
194
|
+
| Response snapshot diffs | ✅ | — | — | — |
|
|
195
|
+
| Benchmarking / latency | — | — | ✅ | — |
|
|
196
|
+
| Jest integration | — | — | — | ✅ |
|
|
197
|
+
| MCP proxy mode | — | ✅ | — | — |
|
|
198
|
+
| Works as MCP server | ✅ | — | — | — |
|
|
199
|
+
|
|
200
|
+
Each tool has strengths. Observatory focuses on regression detection and CI-friendly workflows. mcp-recorder is great as a transparent proxy. MCPBench is the go-to for performance benchmarking. mcp-jest is ideal if you're already in a Jest workflow.
|
|
201
|
+
|
|
202
|
+
## Prior Art
|
|
203
|
+
|
|
204
|
+
The record/replay/verify pattern is inspired by:
|
|
205
|
+
|
|
206
|
+
- [VCR](https://github.com/vcr/vcr) (Ruby) — pioneered cassette-based HTTP record/replay
|
|
207
|
+
- [Polly.js](https://github.com/Netflix/pollyjs) (Netflix) — HTTP interaction recording for JavaScript
|
|
208
|
+
- [mcp-recorder](https://github.com/punkpeye/mcp-recorder) — MCP-specific traffic recording proxy
|
|
209
|
+
- [MCPBench](https://github.com/QuantGeekDev/mcpbench) — MCP server benchmarking
|
|
210
|
+
- [mcp-jest](https://github.com/nicobailon/mcp-jest) — Jest-style testing for MCP servers
|
|
211
|
+
|
|
162
212
|
## Limitations
|
|
163
213
|
|
|
164
214
|
- Servers requiring interactive OAuth (e.g., Google Drive) need pre-authentication before Observatory can connect
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpTargetConfig } from "../types.js";
|
|
2
|
-
import type { AdapterSession } from "./local-process.js";
|
|
2
|
+
import type { AdapterConnectOptions, AdapterSession } from "./local-process.js";
|
|
3
3
|
export declare class HttpAdapter {
|
|
4
|
-
connect(target: HttpTargetConfig): Promise<AdapterSession>;
|
|
4
|
+
connect(target: HttpTargetConfig, options?: AdapterConnectOptions): Promise<AdapterSession>;
|
|
5
5
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { RecordingTransport } from "../transport/recording-transport.js";
|
|
4
5
|
import { formatConnectionFailureDiagnosis } from "../utils/failure-diagnosis.js";
|
|
5
6
|
import { TOOL_VERSION } from "../version.js";
|
|
6
7
|
export class HttpAdapter {
|
|
7
|
-
async connect(target) {
|
|
8
|
+
async connect(target, options) {
|
|
8
9
|
const headers = { ...(target.headers ?? {}) };
|
|
9
10
|
if (target.authToken) {
|
|
10
11
|
headers["Authorization"] = `Bearer ${target.authToken}`;
|
|
@@ -15,9 +16,13 @@ export class HttpAdapter {
|
|
|
15
16
|
const timeoutMs = target.timeoutMs ?? 15_000;
|
|
16
17
|
// Try streamable-http first, fall back to SSE
|
|
17
18
|
let connected = false;
|
|
19
|
+
let activeTransport;
|
|
18
20
|
try {
|
|
19
|
-
|
|
21
|
+
let transport = new StreamableHTTPClientTransport(url, { requestInit: { headers } });
|
|
22
|
+
if (options?.record)
|
|
23
|
+
transport = new RecordingTransport(transport);
|
|
20
24
|
await client.connect(transport, { timeout: timeoutMs });
|
|
25
|
+
activeTransport = transport;
|
|
21
26
|
connected = true;
|
|
22
27
|
}
|
|
23
28
|
catch {
|
|
@@ -25,8 +30,11 @@ export class HttpAdapter {
|
|
|
25
30
|
}
|
|
26
31
|
if (!connected) {
|
|
27
32
|
try {
|
|
28
|
-
|
|
33
|
+
let transport = new SSEClientTransport(url, { requestInit: { headers } });
|
|
34
|
+
if (options?.record)
|
|
35
|
+
transport = new RecordingTransport(transport);
|
|
29
36
|
await client.connect(transport, { timeout: timeoutMs });
|
|
37
|
+
activeTransport = transport;
|
|
30
38
|
connected = true;
|
|
31
39
|
}
|
|
32
40
|
catch (error) {
|
|
@@ -45,6 +53,7 @@ export class HttpAdapter {
|
|
|
45
53
|
serverName: serverVersion?.name,
|
|
46
54
|
serverVersion: serverVersion?.version,
|
|
47
55
|
stderrLines,
|
|
56
|
+
transport: options?.record ? activeTransport : undefined,
|
|
48
57
|
close: async () => {
|
|
49
58
|
await client.close();
|
|
50
59
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/adapters/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/adapters/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
4
|
import type { LocalProcessTargetConfig, TargetConfig } from "../types.js";
|
|
5
|
+
export interface AdapterConnectOptions {
|
|
6
|
+
record?: boolean;
|
|
7
|
+
}
|
|
4
8
|
export interface AdapterSession {
|
|
5
9
|
client: Client;
|
|
6
10
|
serverCapabilities?: ServerCapabilities;
|
|
7
11
|
serverName?: string;
|
|
8
12
|
serverVersion?: string;
|
|
9
13
|
stderrLines: string[];
|
|
14
|
+
/** The transport used for the connection. Present when recording. */
|
|
15
|
+
transport?: Transport;
|
|
10
16
|
close(): Promise<void>;
|
|
11
17
|
}
|
|
12
18
|
export declare class AdapterConnectError extends Error {
|
|
@@ -15,5 +21,5 @@ export declare class AdapterConnectError extends Error {
|
|
|
15
21
|
constructor(target: TargetConfig, rawMessage: string, stderrLines: string[]);
|
|
16
22
|
}
|
|
17
23
|
export declare class LocalProcessAdapter {
|
|
18
|
-
connect(target: LocalProcessTargetConfig): Promise<AdapterSession>;
|
|
24
|
+
connect(target: LocalProcessTargetConfig, options?: AdapterConnectOptions): Promise<AdapterSession>;
|
|
19
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { RecordingTransport } from "../transport/recording-transport.js";
|
|
3
4
|
import { formatConnectionFailureDiagnosis } from "../utils/failure-diagnosis.js";
|
|
4
5
|
import { TOOL_VERSION } from "../version.js";
|
|
5
6
|
export class AdapterConnectError extends Error {
|
|
@@ -13,8 +14,8 @@ export class AdapterConnectError extends Error {
|
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
export class LocalProcessAdapter {
|
|
16
|
-
async connect(target) {
|
|
17
|
-
const
|
|
17
|
+
async connect(target, options) {
|
|
18
|
+
const stdioTransport = new StdioClientTransport({
|
|
18
19
|
command: target.command,
|
|
19
20
|
args: target.args,
|
|
20
21
|
cwd: target.cwd,
|
|
@@ -22,7 +23,7 @@ export class LocalProcessAdapter {
|
|
|
22
23
|
stderr: "pipe"
|
|
23
24
|
});
|
|
24
25
|
const stderrLines = [];
|
|
25
|
-
|
|
26
|
+
stdioTransport.stderr?.on("data", (chunk) => {
|
|
26
27
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
27
28
|
for (const line of text.split(/\r?\n/)) {
|
|
28
29
|
const trimmed = line.trim();
|
|
@@ -31,6 +32,10 @@ export class LocalProcessAdapter {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
});
|
|
35
|
+
// Optionally wrap in RecordingTransport
|
|
36
|
+
const transport = options?.record
|
|
37
|
+
? new RecordingTransport(stdioTransport)
|
|
38
|
+
: stdioTransport;
|
|
34
39
|
const client = new Client({
|
|
35
40
|
name: "mcp-observatory",
|
|
36
41
|
version: TOOL_VERSION
|
|
@@ -54,6 +59,7 @@ export class LocalProcessAdapter {
|
|
|
54
59
|
serverName: serverVersion?.name,
|
|
55
60
|
serverVersion: serverVersion?.version,
|
|
56
61
|
stderrLines,
|
|
62
|
+
transport: options?.record ? transport : undefined,
|
|
57
63
|
close: async () => {
|
|
58
64
|
await client.close();
|
|
59
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-process.js","sourceRoot":"","sources":["../../../src/adapters/local-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"local-process.js","sourceRoot":"","sources":["../../../src/adapters/local-process.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface CassetteEntry {
|
|
2
|
+
direction: "request" | "response" | "notification";
|
|
3
|
+
method: string;
|
|
4
|
+
params?: unknown;
|
|
5
|
+
result?: unknown;
|
|
6
|
+
error?: {
|
|
7
|
+
code: number;
|
|
8
|
+
message: string;
|
|
9
|
+
data?: unknown;
|
|
10
|
+
};
|
|
11
|
+
timestampMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Cassette {
|
|
14
|
+
version: 1;
|
|
15
|
+
targetId: string;
|
|
16
|
+
recordedAt: string;
|
|
17
|
+
transport: "stdio" | "http";
|
|
18
|
+
entries: CassetteEntry[];
|
|
19
|
+
}
|
|
20
|
+
export declare function defaultCassettesDirectory(cwd?: string): string;
|
|
21
|
+
export declare function saveCassette(cassette: Cassette, outDir: string): Promise<string>;
|
|
22
|
+
export declare function loadCassette(filePath: string): Promise<Cassette>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDirectory } from "./storage.js";
|
|
4
|
+
import { slugify } from "./utils/ids.js";
|
|
5
|
+
// ── I/O ──────────────────────────────────────────────────────────────────────
|
|
6
|
+
export function defaultCassettesDirectory(cwd = process.cwd()) {
|
|
7
|
+
return path.join(cwd, ".mcp-observatory", "cassettes");
|
|
8
|
+
}
|
|
9
|
+
export async function saveCassette(cassette, outDir) {
|
|
10
|
+
await ensureDirectory(outDir);
|
|
11
|
+
const timestamp = new Date().toISOString().replaceAll(":", "-");
|
|
12
|
+
const fileName = `${timestamp}--${slugify(cassette.targetId)}.cassette.json`;
|
|
13
|
+
const filePath = path.join(outDir, fileName);
|
|
14
|
+
await writeFile(filePath, JSON.stringify(cassette, null, 2) + "\n", "utf8");
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
export async function loadCassette(filePath) {
|
|
18
|
+
const content = await readFile(filePath, "utf8");
|
|
19
|
+
const data = JSON.parse(content);
|
|
20
|
+
if (typeof data !== "object" ||
|
|
21
|
+
data === null ||
|
|
22
|
+
data["version"] !== 1 ||
|
|
23
|
+
!Array.isArray(data["entries"])) {
|
|
24
|
+
throw new Error(`Invalid cassette file: ${filePath}. Expected { version: 1, entries: [...] }.`);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=cassette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cassette.js","sourceRoot":"","sources":["../../src/cassette.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAqBzC,gFAAgF;AAEhF,MAAM,UAAU,yBAAyB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC3D,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,EAAE,WAAW,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAkB,EAClB,MAAc;IAEd,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,GAAG,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC7C,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,IAAI,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE1C,IACE,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAAgC,CAAC,SAAS,CAAC,KAAK,CAAC;QAClD,CAAC,KAAK,CAAC,OAAO,CAAE,IAAgC,CAAC,SAAS,CAAC,CAAC,EAC5D,CAAC;QACD,MAAM,IAAI,KAAK,CACb,0BAA0B,QAAQ,4CAA4C,CAC/E,CAAC;IACJ,CAAC;IAED,OAAO,IAAgB,CAAC;AAC1B,CAAC"}
|
|
@@ -61,6 +61,7 @@ export async function runToolsInvokeCheck(context) {
|
|
|
61
61
|
responded: true,
|
|
62
62
|
isError,
|
|
63
63
|
error: isError ? JSON.stringify(response.content) : undefined,
|
|
64
|
+
responseContent: response.content,
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
catch (error) {
|
|
@@ -92,6 +93,13 @@ export async function runToolsInvokeCheck(context) {
|
|
|
92
93
|
(tools.length > safeTools.length
|
|
93
94
|
? ` (${tools.length - safeTools.length} skipped — have required params)`
|
|
94
95
|
: "");
|
|
96
|
+
// Build response snapshots for diffing
|
|
97
|
+
const responseSnapshots = {};
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
if (r.responded && r.responseContent !== undefined) {
|
|
100
|
+
responseSnapshots[r.name] = r.responseContent;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
95
103
|
return {
|
|
96
104
|
result: makeCheckResult("tools-invoke", status, performance.now() - startedAt, message, [{
|
|
97
105
|
endpoint: "tools/call",
|
|
@@ -101,6 +109,7 @@ export async function runToolsInvokeCheck(context) {
|
|
|
101
109
|
itemCount: results.length,
|
|
102
110
|
identifiers: results.map((r) => r.name),
|
|
103
111
|
diagnostics,
|
|
112
|
+
responseSnapshots: Object.keys(responseSnapshots).length > 0 ? responseSnapshots : undefined,
|
|
104
113
|
}])
|
|
105
114
|
};
|
|
106
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools-invoke.js","sourceRoot":"","sources":["../../../src/checks/tools-invoke.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI9C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,eAAe,EAGhB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tools-invoke.js","sourceRoot":"","sources":["../../../src/checks/tools-invoke.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI9C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,eAAe,EAGhB,MAAM,WAAW,CAAC;AAWnB,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAqB;IAC7D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IACpC,MAAM,UAAU,GAAG,sBAAsB,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAE/E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,MAAM,EAAE,eAAe,CACrB,cAAc,EACd,aAAa,EACb,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,EAC7B,yCAAyC,EACzC,CAAC;oBACC,QAAQ,EAAE,YAAY;oBACtB,UAAU,EAAE,KAAK;oBACjB,SAAS,EAAE,KAAK;oBAChB,mBAAmB,EAAE,KAAK;oBAC1B,WAAW,EAAE,EAAE;iBAChB,CAAC,CACH;SACF,CAAC;IACJ,CAAC;IAED,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QACvF,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO;YACL,MAAM,EAAE,eAAe,CACrB,cAAc,EACd,MAAM,EACN,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,EAC7B,2CAA2C,GAAG,EAAE,EAChD,CAAC;oBACC,QAAQ,EAAE,YAAY;oBACtB,UAAU,EAAE,IAAI;oBAChB,SAAS,EAAE,KAAK;oBAChB,mBAAmB,EAAE,KAAK;oBAC1B,WAAW,EAAE,CAAC,GAAG,CAAC;iBACnB,CAAC,CACH;SACF,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACnC,cAAc,CAAC;QACb,WAAW,EAAE,CAAC,CAAC,WAAkD;QACjE,WAAW,EAAE,CAAC,CAAC,WAAkD;KAClE,CAAC,CACH,CAAC;IAEF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,MAAM,EAAE,eAAe,CACrB,cAAc,EACd,MAAM,EACN,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,EAC7B,GAAG,KAAK,CAAC,MAAM,wGAAwG,EACvH,CAAC;oBACC,QAAQ,EAAE,YAAY;oBACtB,UAAU,EAAE,IAAI;oBAChB,SAAS,EAAE,IAAI;oBACf,mBAAmB,EAAE,IAAI;oBACzB,SAAS,EAAE,CAAC;oBACZ,WAAW,EAAE,EAAE;oBACf,WAAW,EAAE,EAAE;iBAChB,CAAC,CACH;SACF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,WAAkD,CAAC,CAAC;QACrF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,CAC5C,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EACpC,SAAS,EACT,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAC/B,CAAC;YACF,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC;YAC1C,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,IAAI;gBACf,OAAO;gBACP,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC7D,eAAe,EAAE,QAAQ,CAAC,OAAO;aAClC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnE,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,KAAK;gBAChB,OAAO,EAAE,IAAI;gBACb,KAAK,EAAE,GAAG;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,IAAI,aAAa,EAAE,CAAC,CAAC;IAEhF,IAAI,MAAmC,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;SAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GACX,WAAW,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC,MAAM,iBAAiB;QAC9D,GAAG,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,MAAM,SAAS;QAClD,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM;YAC9B,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,kCAAkC;YACxE,CAAC,CAAC,EAAE,CAAC,CAAC;IAEV,uCAAuC;IACvC,MAAM,iBAAiB,GAA4B,EAAE,CAAC;IACtD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YACnD,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,eAAe,CAAC;QAChD,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,eAAe,CACrB,cAAc,EACd,MAAM,EACN,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,EAC7B,OAAO,EACP,CAAC;gBACC,QAAQ,EAAE,YAAY;gBACtB,UAAU,EAAE,IAAI;gBAChB,SAAS,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;gBAC9B,mBAAmB,EAAE,IAAI;gBACzB,SAAS,EAAE,OAAO,CAAC,MAAM;gBACzB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACvC,WAAW;gBACX,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS;aAC7F,CAAC,CACH;KACF,CAAC;AACJ,CAAC"}
|