@ikuma.cloud/pix-mcp 0.0.8 → 0.0.9
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 +110 -39
- package/mcp.schema.json +1 -1
- package/package.json +4 -2
- package/src/client.ts +335 -9
- package/src/command.ts +69 -0
- package/src/config.ts +1 -1
- package/src/index.ts +262 -13
- package/src/output.ts +9 -10
- package/src/prompts.ts +18 -70
- package/src/resource-picker.ts +157 -0
- package/src/resources.ts +769 -0
package/src/client.ts
CHANGED
|
@@ -9,14 +9,25 @@ import {
|
|
|
9
9
|
CallToolResultSchema,
|
|
10
10
|
GetPromptResultSchema,
|
|
11
11
|
ListPromptsResultSchema,
|
|
12
|
+
ListResourcesResultSchema,
|
|
13
|
+
ListResourceTemplatesResultSchema,
|
|
12
14
|
ListToolsResultSchema,
|
|
13
15
|
PromptListChangedNotificationSchema,
|
|
16
|
+
ReadResourceResultSchema,
|
|
17
|
+
ResourceListChangedNotificationSchema,
|
|
14
18
|
ToolSchema,
|
|
15
19
|
ToolListChangedNotificationSchema,
|
|
16
20
|
type Prompt,
|
|
21
|
+
type Resource,
|
|
22
|
+
type ResourceTemplate,
|
|
17
23
|
type Tool,
|
|
18
24
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
19
25
|
import type { ServerConfig } from "./config.ts";
|
|
26
|
+
import {
|
|
27
|
+
validateResource,
|
|
28
|
+
validateResourceTemplate,
|
|
29
|
+
validateResourceUri,
|
|
30
|
+
} from "./resources.ts";
|
|
20
31
|
import { compileSchema, schemaValidator } from "./schema.ts";
|
|
21
32
|
|
|
22
33
|
// SDK 1.30's property decoder incorrectly requires every property schema to be
|
|
@@ -39,27 +50,97 @@ const promptIdentifier = (value: string) =>
|
|
|
39
50
|
Buffer.byteLength(value) <= 256 &&
|
|
40
51
|
!/[\p{C}\p{Zl}\p{Zp}]/u.test(value);
|
|
41
52
|
|
|
53
|
+
// Leave bounded room for the JSON-RPC envelope around a maximum-size result.
|
|
54
|
+
const MAX_HTTP_RESPONSE_BYTES = 16 * 1024 * 1024 + 64 * 1024;
|
|
55
|
+
|
|
56
|
+
function responseLimitTransform(sse: boolean) {
|
|
57
|
+
let responseBytes = 0;
|
|
58
|
+
let eventBytes = 0;
|
|
59
|
+
let lineBytes = 0;
|
|
60
|
+
let previousCarriageReturn = false;
|
|
61
|
+
return new TransformStream<Uint8Array, Uint8Array>({
|
|
62
|
+
transform(chunk, controller) {
|
|
63
|
+
if (!sse) {
|
|
64
|
+
responseBytes += chunk.byteLength;
|
|
65
|
+
if (responseBytes > MAX_HTTP_RESPONSE_BYTES)
|
|
66
|
+
throw new Error("MCP HTTP response exceeded the size limit.");
|
|
67
|
+
} else {
|
|
68
|
+
for (const byte of chunk) {
|
|
69
|
+
eventBytes++;
|
|
70
|
+
if (eventBytes > MAX_HTTP_RESPONSE_BYTES)
|
|
71
|
+
throw new Error("MCP HTTP event exceeded the size limit.");
|
|
72
|
+
if (byte === 0x0d) {
|
|
73
|
+
if (lineBytes === 0) eventBytes = 0;
|
|
74
|
+
lineBytes = 0;
|
|
75
|
+
previousCarriageReturn = true;
|
|
76
|
+
} else if (byte === 0x0a) {
|
|
77
|
+
if (!previousCarriageReturn) {
|
|
78
|
+
if (lineBytes === 0) eventBytes = 0;
|
|
79
|
+
lineBytes = 0;
|
|
80
|
+
}
|
|
81
|
+
previousCarriageReturn = false;
|
|
82
|
+
} else {
|
|
83
|
+
previousCarriageReturn = false;
|
|
84
|
+
lineBytes++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
controller.enqueue(chunk);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function boundedHttpResponse(response: Response): Response {
|
|
94
|
+
if (!response.body) return response;
|
|
95
|
+
const sse =
|
|
96
|
+
mediaTypeEssence(response.headers.get("content-type")) ===
|
|
97
|
+
"text/event-stream";
|
|
98
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
99
|
+
if (
|
|
100
|
+
!sse &&
|
|
101
|
+
Number.isFinite(declaredLength) &&
|
|
102
|
+
declaredLength > MAX_HTTP_RESPONSE_BYTES
|
|
103
|
+
) {
|
|
104
|
+
void response.body.cancel();
|
|
105
|
+
throw new Error("MCP HTTP response exceeded the size limit.");
|
|
106
|
+
}
|
|
107
|
+
return new Response(response.body.pipeThrough(responseLimitTransform(sse)), {
|
|
108
|
+
status: response.status,
|
|
109
|
+
statusText: response.statusText,
|
|
110
|
+
headers: response.headers,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
42
114
|
export class Connection {
|
|
43
115
|
readonly client: Client;
|
|
44
116
|
readonly config: ServerConfig;
|
|
45
117
|
status = "Not connected";
|
|
46
118
|
toolStatus = "Not discovered";
|
|
47
119
|
promptStatus = "Not discovered";
|
|
120
|
+
resourceStatus = "Not discovered";
|
|
48
121
|
instructions = "";
|
|
49
122
|
#transport: StdioClientTransport | StreamableHTTPClientTransport;
|
|
50
123
|
#lifetime = new AbortController();
|
|
51
124
|
#initializing: Promise<void> | undefined;
|
|
52
125
|
#refreshing: Promise<void> | undefined;
|
|
53
126
|
#promptRefreshing: Promise<void> | undefined;
|
|
127
|
+
#resourceRefreshing: Promise<void> | undefined;
|
|
54
128
|
#closing: Promise<void> | undefined;
|
|
55
129
|
#dirty = false;
|
|
56
130
|
#promptDirty = false;
|
|
131
|
+
#resourceDirty = false;
|
|
57
132
|
#stopped = false;
|
|
58
133
|
#connected = false;
|
|
59
134
|
#toolsReady = false;
|
|
60
135
|
#promptsReady = false;
|
|
136
|
+
#resourcesReady = false;
|
|
137
|
+
#resourcesSupported = false;
|
|
61
138
|
#changed: (tools: Tool[]) => void;
|
|
62
139
|
#promptsChanged: (prompts: Prompt[]) => void;
|
|
140
|
+
#resourcesChanged: (
|
|
141
|
+
resources: Resource[],
|
|
142
|
+
templates: ResourceTemplate[],
|
|
143
|
+
) => void;
|
|
63
144
|
#promptNames = new Set<string>();
|
|
64
145
|
#outputValidators = new Map<string, ReturnType<typeof compileSchema>>();
|
|
65
146
|
#operation = new AsyncLocalStorage<AbortSignal>();
|
|
@@ -68,10 +149,15 @@ export class Connection {
|
|
|
68
149
|
config: ServerConfig,
|
|
69
150
|
changed: (tools: Tool[]) => void,
|
|
70
151
|
promptsChanged: (prompts: Prompt[]) => void = () => {},
|
|
152
|
+
resourcesChanged: (
|
|
153
|
+
resources: Resource[],
|
|
154
|
+
templates: ResourceTemplate[],
|
|
155
|
+
) => void = () => {},
|
|
71
156
|
) {
|
|
72
157
|
this.config = config;
|
|
73
158
|
this.#changed = changed;
|
|
74
159
|
this.#promptsChanged = promptsChanged;
|
|
160
|
+
this.#resourcesChanged = resourcesChanged;
|
|
75
161
|
this.client = new Client(
|
|
76
162
|
{ name: "pix-mcp", version: "0.0.0" },
|
|
77
163
|
{ capabilities: {}, jsonSchemaValidator: schemaValidator },
|
|
@@ -122,6 +208,17 @@ export class Connection {
|
|
|
122
208
|
}
|
|
123
209
|
},
|
|
124
210
|
);
|
|
211
|
+
this.client.setNotificationHandler(
|
|
212
|
+
ResourceListChangedNotificationSchema,
|
|
213
|
+
async () => {
|
|
214
|
+
if (this.#stopped) return;
|
|
215
|
+
try {
|
|
216
|
+
await this.refreshResources();
|
|
217
|
+
} catch {
|
|
218
|
+
/* refresh already clears the stale catalog */
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
);
|
|
125
222
|
}
|
|
126
223
|
|
|
127
224
|
#disconnect() {
|
|
@@ -129,9 +226,12 @@ export class Connection {
|
|
|
129
226
|
this.status = "Disconnected; reload Pi to reconnect";
|
|
130
227
|
this.toolStatus = "Unavailable";
|
|
131
228
|
this.promptStatus = "Unavailable";
|
|
229
|
+
this.resourceStatus = "Unavailable";
|
|
132
230
|
this.#connected = false;
|
|
133
231
|
this.#toolsReady = false;
|
|
134
232
|
this.#promptsReady = false;
|
|
233
|
+
this.#resourcesReady = false;
|
|
234
|
+
this.#resourcesSupported = false;
|
|
135
235
|
this.#lifetime.abort(
|
|
136
236
|
new Error("MCP connection is unavailable; reload Pi to reconnect."),
|
|
137
237
|
);
|
|
@@ -139,16 +239,20 @@ export class Connection {
|
|
|
139
239
|
this.#promptNames.clear();
|
|
140
240
|
this.#changed([]);
|
|
141
241
|
this.#promptsChanged([]);
|
|
242
|
+
this.#resourcesChanged([], []);
|
|
142
243
|
void this.client.close().catch(() => {});
|
|
143
244
|
}
|
|
144
245
|
|
|
145
246
|
#updateStatus() {
|
|
146
247
|
if (!this.#connected || this.#stopped) return;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this
|
|
150
|
-
|
|
151
|
-
|
|
248
|
+
const failures = [
|
|
249
|
+
...(!this.#toolsReady ? ["tool"] : []),
|
|
250
|
+
...(!this.#promptsReady ? ["prompt"] : []),
|
|
251
|
+
...(!this.#resourcesReady ? ["resource"] : []),
|
|
252
|
+
];
|
|
253
|
+
if (failures.length === 0) this.status = "Connected";
|
|
254
|
+
else if (failures.length < 3)
|
|
255
|
+
this.status = `Connected; ${failures.join(" and ")} discovery failed`;
|
|
152
256
|
else this.status = "Discovery failed; reload Pi to retry";
|
|
153
257
|
}
|
|
154
258
|
|
|
@@ -211,7 +315,8 @@ export class Connection {
|
|
|
211
315
|
) as unknown as Promise<Response>;
|
|
212
316
|
};
|
|
213
317
|
// Teardown must still send DELETE after the connection lifetime is aborted.
|
|
214
|
-
if (init?.method === "DELETE")
|
|
318
|
+
if (init?.method === "DELETE")
|
|
319
|
+
return boundedHttpResponse(await fetch(AbortSignal.timeout(2000)));
|
|
215
320
|
const signals = [
|
|
216
321
|
this.#lifetime.signal,
|
|
217
322
|
...(init?.signal ? [init.signal] : []),
|
|
@@ -229,8 +334,10 @@ export class Connection {
|
|
|
229
334
|
(("method" in message && "id" in message) ||
|
|
230
335
|
message.method === "notifications/initialized");
|
|
231
336
|
const operation = scoped ? this.#operation.getStore() : undefined;
|
|
232
|
-
return
|
|
233
|
-
|
|
337
|
+
return boundedHttpResponse(
|
|
338
|
+
await fetch(
|
|
339
|
+
AbortSignal.any([...signals, operation ?? AbortSignal.timeout(2000)]),
|
|
340
|
+
),
|
|
234
341
|
);
|
|
235
342
|
}
|
|
236
343
|
// GET is optional (405 is valid), but an established notification stream
|
|
@@ -254,7 +361,7 @@ export class Connection {
|
|
|
254
361
|
await response.body?.cancel();
|
|
255
362
|
throw new Error("MCP notification stream unavailable.");
|
|
256
363
|
}
|
|
257
|
-
const stream =
|
|
364
|
+
const stream = responseLimitTransform(true);
|
|
258
365
|
void response.body.pipeTo(stream.writable).then(
|
|
259
366
|
() => this.#disconnect(),
|
|
260
367
|
() => this.#disconnect(),
|
|
@@ -312,8 +419,10 @@ export class Connection {
|
|
|
312
419
|
"Connection failed; check configuration/authentication and reload Pi";
|
|
313
420
|
this.toolStatus = "Unavailable";
|
|
314
421
|
this.promptStatus = "Unavailable";
|
|
422
|
+
this.resourceStatus = "Unavailable";
|
|
315
423
|
this.#changed([]);
|
|
316
424
|
this.#promptsChanged([]);
|
|
425
|
+
this.#resourcesChanged([], []);
|
|
317
426
|
}
|
|
318
427
|
await this.client.close().catch(() => {});
|
|
319
428
|
throw new Error(`MCP server ${this.config.name}: connection failed.`);
|
|
@@ -321,6 +430,7 @@ export class Connection {
|
|
|
321
430
|
const results = await Promise.allSettled([
|
|
322
431
|
this.refresh(),
|
|
323
432
|
this.refreshPrompts(),
|
|
433
|
+
this.refreshResources(),
|
|
324
434
|
]);
|
|
325
435
|
if (results.some((result) => result.status === "rejected"))
|
|
326
436
|
throw new Error(`MCP server ${this.config.name}: discovery failed.`);
|
|
@@ -551,6 +661,217 @@ export class Connection {
|
|
|
551
661
|
}
|
|
552
662
|
}
|
|
553
663
|
|
|
664
|
+
refreshResources(): Promise<void> {
|
|
665
|
+
this.#resourceDirty = true;
|
|
666
|
+
// Do not let picker selections remain current while either half of the
|
|
667
|
+
// atomic resource/template snapshot is being refreshed.
|
|
668
|
+
if (this.#resourcesReady) {
|
|
669
|
+
this.#resourcesReady = false;
|
|
670
|
+
this.resourceStatus = "Loading";
|
|
671
|
+
this.#resourcesChanged([], []);
|
|
672
|
+
}
|
|
673
|
+
this.#resourceRefreshing ??= this.#drainResourceRefreshes();
|
|
674
|
+
return this.#resourceRefreshing;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async #drainResourceRefreshes(): Promise<void> {
|
|
678
|
+
let failed = false;
|
|
679
|
+
let failure: unknown;
|
|
680
|
+
try {
|
|
681
|
+
await this.#refreshResources();
|
|
682
|
+
} catch (error) {
|
|
683
|
+
failed = true;
|
|
684
|
+
failure = error;
|
|
685
|
+
}
|
|
686
|
+
this.#resourceRefreshing = undefined;
|
|
687
|
+
if (this.#resourceDirty && !this.#stopped) return this.refreshResources();
|
|
688
|
+
if (failed) throw failure;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async #refreshResources(): Promise<void> {
|
|
692
|
+
try {
|
|
693
|
+
while (this.#resourceDirty && !this.#stopped) {
|
|
694
|
+
this.#resourceDirty = false;
|
|
695
|
+
const resources: Resource[] = [];
|
|
696
|
+
const templates: ResourceTemplate[] = [];
|
|
697
|
+
const resourceCursors = new Set<string>();
|
|
698
|
+
const templateCursors = new Set<string>();
|
|
699
|
+
const uris = new Set<string>();
|
|
700
|
+
const uriTemplates = new Set<string>();
|
|
701
|
+
const supportsResources = Boolean(
|
|
702
|
+
this.client.getServerCapabilities()?.resources,
|
|
703
|
+
);
|
|
704
|
+
let bytes = 0;
|
|
705
|
+
await this.#deadline(
|
|
706
|
+
this.config.catalogTimeoutMs,
|
|
707
|
+
undefined,
|
|
708
|
+
async (signal) => {
|
|
709
|
+
if (supportsResources) {
|
|
710
|
+
let cursor: string | undefined;
|
|
711
|
+
do {
|
|
712
|
+
const page = await this.#deadline(
|
|
713
|
+
this.config.catalogTimeoutMs,
|
|
714
|
+
signal,
|
|
715
|
+
(requestSignal) =>
|
|
716
|
+
this.client.request(
|
|
717
|
+
{
|
|
718
|
+
method: "resources/list",
|
|
719
|
+
params: cursor === undefined ? {} : { cursor },
|
|
720
|
+
},
|
|
721
|
+
ListResourcesResultSchema,
|
|
722
|
+
{
|
|
723
|
+
signal: requestSignal,
|
|
724
|
+
timeout: this.config.catalogTimeoutMs,
|
|
725
|
+
},
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
bytes += Buffer.byteLength(JSON.stringify(page));
|
|
729
|
+
if (
|
|
730
|
+
bytes > 2 * 1024 * 1024 ||
|
|
731
|
+
resources.length + templates.length + page.resources.length >
|
|
732
|
+
1000
|
|
733
|
+
)
|
|
734
|
+
throw new Error("Catalog limit");
|
|
735
|
+
for (const resource of page.resources) {
|
|
736
|
+
validateResource(resource);
|
|
737
|
+
if (uris.has(resource.uri))
|
|
738
|
+
throw new Error("Duplicate resource URI");
|
|
739
|
+
uris.add(resource.uri);
|
|
740
|
+
const catalogResource = { ...resource };
|
|
741
|
+
delete catalogResource._meta;
|
|
742
|
+
delete catalogResource.icons;
|
|
743
|
+
resources.push(catalogResource);
|
|
744
|
+
}
|
|
745
|
+
cursor = page.nextCursor;
|
|
746
|
+
if (cursor !== undefined) {
|
|
747
|
+
if (
|
|
748
|
+
resourceCursors.has(cursor) ||
|
|
749
|
+
resourceCursors.size >= 100
|
|
750
|
+
)
|
|
751
|
+
throw new Error("Invalid pagination");
|
|
752
|
+
resourceCursors.add(cursor);
|
|
753
|
+
}
|
|
754
|
+
} while (cursor !== undefined);
|
|
755
|
+
|
|
756
|
+
cursor = undefined;
|
|
757
|
+
do {
|
|
758
|
+
const page = await this.#deadline(
|
|
759
|
+
this.config.catalogTimeoutMs,
|
|
760
|
+
signal,
|
|
761
|
+
(requestSignal) =>
|
|
762
|
+
this.client.request(
|
|
763
|
+
{
|
|
764
|
+
method: "resources/templates/list",
|
|
765
|
+
params: cursor === undefined ? {} : { cursor },
|
|
766
|
+
},
|
|
767
|
+
ListResourceTemplatesResultSchema,
|
|
768
|
+
{
|
|
769
|
+
signal: requestSignal,
|
|
770
|
+
timeout: this.config.catalogTimeoutMs,
|
|
771
|
+
},
|
|
772
|
+
),
|
|
773
|
+
);
|
|
774
|
+
bytes += Buffer.byteLength(JSON.stringify(page));
|
|
775
|
+
if (
|
|
776
|
+
bytes > 2 * 1024 * 1024 ||
|
|
777
|
+
resources.length +
|
|
778
|
+
templates.length +
|
|
779
|
+
page.resourceTemplates.length >
|
|
780
|
+
1000
|
|
781
|
+
)
|
|
782
|
+
throw new Error("Catalog limit");
|
|
783
|
+
for (const template of page.resourceTemplates) {
|
|
784
|
+
validateResourceTemplate(template);
|
|
785
|
+
if (uriTemplates.has(template.uriTemplate))
|
|
786
|
+
throw new Error("Duplicate resource URI template");
|
|
787
|
+
uriTemplates.add(template.uriTemplate);
|
|
788
|
+
const catalogTemplate = { ...template };
|
|
789
|
+
delete catalogTemplate._meta;
|
|
790
|
+
delete catalogTemplate.icons;
|
|
791
|
+
templates.push(catalogTemplate);
|
|
792
|
+
}
|
|
793
|
+
cursor = page.nextCursor;
|
|
794
|
+
if (cursor !== undefined) {
|
|
795
|
+
if (
|
|
796
|
+
templateCursors.has(cursor) ||
|
|
797
|
+
templateCursors.size >= 100
|
|
798
|
+
)
|
|
799
|
+
throw new Error("Invalid pagination");
|
|
800
|
+
templateCursors.add(cursor);
|
|
801
|
+
}
|
|
802
|
+
} while (cursor !== undefined);
|
|
803
|
+
}
|
|
804
|
+
signal.throwIfAborted();
|
|
805
|
+
// One notification covers both list endpoints. Never publish a
|
|
806
|
+
// cross-revision pair when it arrives between those requests.
|
|
807
|
+
if (!this.#stopped && !this.#resourceDirty) {
|
|
808
|
+
this.#resourcesReady = true;
|
|
809
|
+
this.#resourcesSupported = supportsResources;
|
|
810
|
+
this.resourceStatus = supportsResources
|
|
811
|
+
? "Available"
|
|
812
|
+
: "Not supported";
|
|
813
|
+
this.#resourcesChanged(resources, templates);
|
|
814
|
+
this.#updateStatus();
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
if (!this.#stopped) {
|
|
821
|
+
this.#resourcesReady = false;
|
|
822
|
+
this.#resourcesSupported = false;
|
|
823
|
+
this.resourceStatus = "Discovery failed; reload Pi to retry";
|
|
824
|
+
this.#resourcesChanged([], []);
|
|
825
|
+
this.#updateStatus();
|
|
826
|
+
}
|
|
827
|
+
throw new Error(
|
|
828
|
+
`MCP server ${this.config.name}: resource discovery failed.`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async readResource(uri: string, signal?: AbortSignal) {
|
|
834
|
+
await this.start().catch(() => {});
|
|
835
|
+
const combined = signal
|
|
836
|
+
? AbortSignal.any([signal, this.#lifetime.signal])
|
|
837
|
+
: this.#lifetime.signal;
|
|
838
|
+
combined.throwIfAborted();
|
|
839
|
+
if (!this.#resourcesReady)
|
|
840
|
+
throw new Error(
|
|
841
|
+
"MCP resource catalog is unavailable; reload Pi to retry.",
|
|
842
|
+
);
|
|
843
|
+
if (!this.#resourcesSupported)
|
|
844
|
+
throw new Error("MCP server does not support resources.");
|
|
845
|
+
validateResourceUri(uri);
|
|
846
|
+
try {
|
|
847
|
+
const result = await this.#deadline(
|
|
848
|
+
this.config.timeout,
|
|
849
|
+
combined,
|
|
850
|
+
(requestSignal) =>
|
|
851
|
+
this.client.request(
|
|
852
|
+
{ method: "resources/read", params: { uri } },
|
|
853
|
+
ReadResourceResultSchema,
|
|
854
|
+
{
|
|
855
|
+
signal: requestSignal,
|
|
856
|
+
timeout: this.config.timeout,
|
|
857
|
+
resetTimeoutOnProgress: false,
|
|
858
|
+
},
|
|
859
|
+
),
|
|
860
|
+
);
|
|
861
|
+
if (
|
|
862
|
+
result.contents.length > 100 ||
|
|
863
|
+
Buffer.byteLength(JSON.stringify(result)) > 16 * 1024 * 1024
|
|
864
|
+
)
|
|
865
|
+
throw new Error("Resource result limit");
|
|
866
|
+
return result;
|
|
867
|
+
} catch {
|
|
868
|
+
combined.throwIfAborted();
|
|
869
|
+
throw new Error(
|
|
870
|
+
`MCP server ${this.config.name}: resource request failed or timed out.`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
554
875
|
async getPrompt(
|
|
555
876
|
name: string,
|
|
556
877
|
args: Record<string, string> | undefined,
|
|
@@ -656,12 +977,16 @@ export class Connection {
|
|
|
656
977
|
this.status = "Closed";
|
|
657
978
|
this.toolStatus = "Closed";
|
|
658
979
|
this.promptStatus = "Closed";
|
|
980
|
+
this.resourceStatus = "Closed";
|
|
659
981
|
this.#toolsReady = false;
|
|
660
982
|
this.#promptsReady = false;
|
|
983
|
+
this.#resourcesReady = false;
|
|
984
|
+
this.#resourcesSupported = false;
|
|
661
985
|
this.#outputValidators.clear();
|
|
662
986
|
this.#promptNames.clear();
|
|
663
987
|
this.#changed([]);
|
|
664
988
|
this.#promptsChanged([]);
|
|
989
|
+
this.#resourcesChanged([], []);
|
|
665
990
|
if (
|
|
666
991
|
this.#transport instanceof StreamableHTTPClientTransport &&
|
|
667
992
|
this.#transport.sessionId
|
|
@@ -672,5 +997,6 @@ export class Connection {
|
|
|
672
997
|
await this.#initializing?.catch(() => {});
|
|
673
998
|
await this.#refreshing?.catch(() => {});
|
|
674
999
|
await this.#promptRefreshing?.catch(() => {});
|
|
1000
|
+
await this.#resourceRefreshing?.catch(() => {});
|
|
675
1001
|
}
|
|
676
1002
|
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface CommandToken {
|
|
2
|
+
value: string;
|
|
3
|
+
separator?: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function quoteCommandArgument(value: string): string {
|
|
7
|
+
return /^[A-Za-z0-9_.:/-]+$/.test(value)
|
|
8
|
+
? value
|
|
9
|
+
: `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderCommandToken(token: CommandToken): string {
|
|
13
|
+
if (token.separator === undefined) return quoteCommandArgument(token.value);
|
|
14
|
+
const name = quoteCommandArgument(token.value.slice(0, token.separator));
|
|
15
|
+
const value = token.value.slice(token.separator + 1);
|
|
16
|
+
return `${name}=${value === "" ? "" : quoteCommandArgument(value)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function tokenizeCommand(
|
|
20
|
+
input: string,
|
|
21
|
+
subject = "command",
|
|
22
|
+
): CommandToken[] {
|
|
23
|
+
const tokens: CommandToken[] = [];
|
|
24
|
+
let token = "";
|
|
25
|
+
let separator: number | undefined;
|
|
26
|
+
let quote: '"' | "'" | undefined;
|
|
27
|
+
let escaped = false;
|
|
28
|
+
let started = false;
|
|
29
|
+
for (const character of input) {
|
|
30
|
+
if (escaped) {
|
|
31
|
+
token += character;
|
|
32
|
+
escaped = false;
|
|
33
|
+
started = true;
|
|
34
|
+
} else if (character === "\\" && quote !== "'") {
|
|
35
|
+
escaped = true;
|
|
36
|
+
started = true;
|
|
37
|
+
} else if (quote) {
|
|
38
|
+
if (character === quote) quote = undefined;
|
|
39
|
+
else token += character;
|
|
40
|
+
started = true;
|
|
41
|
+
} else if (character === '"' || character === "'") {
|
|
42
|
+
quote = character;
|
|
43
|
+
started = true;
|
|
44
|
+
} else if (/\s/u.test(character)) {
|
|
45
|
+
if (started) {
|
|
46
|
+
tokens.push({
|
|
47
|
+
value: token,
|
|
48
|
+
...(separator !== undefined ? { separator } : {}),
|
|
49
|
+
});
|
|
50
|
+
token = "";
|
|
51
|
+
separator = undefined;
|
|
52
|
+
started = false;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (character === "=" && separator === undefined)
|
|
56
|
+
separator = token.length;
|
|
57
|
+
token += character;
|
|
58
|
+
started = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (quote) throw new Error(`Unterminated quote in ${subject} arguments.`);
|
|
62
|
+
if (escaped) throw new Error(`Trailing escape in ${subject} arguments.`);
|
|
63
|
+
if (started)
|
|
64
|
+
tokens.push({
|
|
65
|
+
value: token,
|
|
66
|
+
...(separator !== undefined ? { separator } : {}),
|
|
67
|
+
});
|
|
68
|
+
return tokens;
|
|
69
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -130,7 +130,7 @@ function parseServer(
|
|
|
130
130
|
if (Object.hasOwn(server, "timeoutMs"))
|
|
131
131
|
invalid(
|
|
132
132
|
"timeoutMs",
|
|
133
|
-
"Removed; use timeout for tool calls, startupTimeoutMs for initialization
|
|
133
|
+
"Removed; use timeout for tool calls, prompt retrievals, and resource reads; startupTimeoutMs for initialization; and catalogTimeoutMs for discovery (milliseconds).",
|
|
134
134
|
);
|
|
135
135
|
if (Object.hasOwn(server, "approve"))
|
|
136
136
|
invalid(
|