@procedures/client 0.37.0 → 0.39.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/CHANGELOG.md +43 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/handler.js +3 -2
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +18 -6
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/validation/types.d.ts.map +1 -1
- package/dist/client/validation/types.js +1 -0
- package/dist/client/validation/types.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +1 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/envelope.d.ts.map +1 -1
- package/dist/procedures/envelope.js +2 -0
- package/dist/procedures/envelope.js.map +1 -1
- package/dist/procedures/policy.js +2 -2
- package/dist/procedures/policy.js.map +1 -1
- package/dist/procedures/program.d.ts.map +1 -1
- package/dist/procedures/program.js +3 -0
- package/dist/procedures/program.js.map +1 -1
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +1 -0
- package/dist/procedures/ref.js.map +1 -1
- package/dist/procedures/types.d.ts +1 -1
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +0 -9
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +2 -9
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/stream-metadata.d.ts +15 -0
- package/dist/server/stream-metadata.d.ts.map +1 -0
- package/dist/server/stream-metadata.js +15 -0
- package/dist/server/stream-metadata.js.map +1 -0
- package/dist/testing/index.d.ts +13 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/log-storage.d.ts +28 -0
- package/dist/testing/log-storage.d.ts.map +1 -0
- package/dist/testing/log-storage.js +75 -0
- package/dist/testing/log-storage.js.map +1 -0
- package/dist/testing/mcp-caller.d.ts +33 -0
- package/dist/testing/mcp-caller.d.ts.map +1 -0
- package/dist/testing/mcp-caller.js +97 -0
- package/dist/testing/mcp-caller.js.map +1 -0
- package/dist/testing/transport-contract.d.ts +167 -0
- package/dist/testing/transport-contract.d.ts.map +1 -0
- package/dist/testing/transport-contract.js +387 -0
- package/dist/testing/transport-contract.js.map +1 -0
- package/package.json +5 -1
- package/src/adapters/http/server/handler.ts +3 -2
- package/src/adapters/websocket/client/transport.ts +20 -7
- package/src/browser.ts +2 -1
- package/src/client/validation/types.ts +1 -0
- package/src/procedures/collection/schemas.ts +1 -0
- package/src/procedures/envelope.ts +2 -0
- package/src/procedures/policy.ts +2 -2
- package/src/procedures/program.ts +3 -0
- package/src/procedures/ref.ts +1 -0
- package/src/procedures/types.ts +1 -1
- package/src/server/index.ts +2 -1
- package/src/server/procedure-server.ts +2 -11
- package/src/server/stream-metadata.ts +16 -0
- package/src/testing/index.ts +31 -0
- package/src/testing/log-storage.ts +72 -0
- package/src/testing/mcp-caller.ts +100 -0
- package/src/testing/transport-contract.ts +560 -0
package/src/procedures/types.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ProcedurePath = string[];
|
|
|
38
38
|
* `%` as `%25` and `.` as `%2E` (`encodeSegment()`, roadmap CLI-75). Thus the key of
|
|
39
39
|
* `["a.b", "c"]` is `a%2Eb.c`, and not the key of `["a", "b", "c"]`. A plain path keeps its key.
|
|
40
40
|
*/
|
|
41
|
-
export function pathToKey(path:
|
|
41
|
+
export function pathToKey(path: readonly string[]): string {
|
|
42
42
|
return segmentsToTopic(path);
|
|
43
43
|
}
|
|
44
44
|
|
package/src/server/index.ts
CHANGED
|
@@ -43,7 +43,8 @@ export type { ServerOptions } from "./server.js";
|
|
|
43
43
|
export type { ServerErrorPolicy, ExposedError, ErrorCall } from "./error-policy.js";
|
|
44
44
|
export { exposeError, lastResortError, HIDDEN_ERROR_MESSAGE, DEFAULT_KEPT_METADATA } from "./error-policy.js";
|
|
45
45
|
|
|
46
|
-
export { ProcedureServer, createProcedureServer
|
|
46
|
+
export { ProcedureServer, createProcedureServer } from "./procedure-server.js";
|
|
47
|
+
export { STREAM_METADATA_KEY, isUnboundedStream } from "./stream-metadata.js";
|
|
47
48
|
export type { ProcedureServerOptions, RegisterProcedureOptions, StorageConfig, StopOptions, StopParts, StorageHandle, UseStorageOptions } from "./procedure-server.js";
|
|
48
49
|
export { pathToMethod, methodToPath, methodName } from "./method.js";
|
|
49
50
|
// The escape of a segment with a dot, for a method name, a key and a topic (roadmap CLI-75)
|
|
@@ -12,6 +12,7 @@ export { errorResponse };
|
|
|
12
12
|
import type { Admission, AmbientCredentials, Credentials, Method, Principal, PrincipalOf, RequestIdentity, ServerHandler, ServerRequest, ServerResponse } from "./types.js";
|
|
13
13
|
import { methodToPath, pathToMethod } from "./method.js";
|
|
14
14
|
import { isLocalCredentials } from "./request-auth.js";
|
|
15
|
+
import { STREAM_METADATA_KEY } from "./stream-metadata.js";
|
|
15
16
|
import type {
|
|
16
17
|
AnyProcedure,
|
|
17
18
|
ProcedurePath,
|
|
@@ -230,17 +231,6 @@ export interface RegisterProcedureOptions {
|
|
|
230
231
|
alwaysOn?: boolean | undefined;
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
/**
|
|
234
|
-
* The metadata key of a stream response whose procedure declares `stream: { unbounded: true }`.
|
|
235
|
-
* Its value is "unbounded".
|
|
236
|
-
*/
|
|
237
|
-
export const STREAM_METADATA_KEY = "stream";
|
|
238
|
-
|
|
239
|
-
/** True when a stream response can run without an end (roadmap R15). */
|
|
240
|
-
export function isUnboundedStream(response: { metadata?: Record<string, unknown> | undefined }): boolean {
|
|
241
|
-
return response.metadata?.[STREAM_METADATA_KEY] === "unbounded";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
234
|
/**
|
|
245
235
|
* True when the presented token is the expected one. The comparison reads each character of the
|
|
246
236
|
* expected token for each presented token, so its time depends only on the expected token. The
|
|
@@ -1274,6 +1264,7 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1274
1264
|
else byStorage.set(storage, [name]);
|
|
1275
1265
|
}
|
|
1276
1266
|
return Array.from(byStorage, ([storage, collections]) => ({
|
|
1267
|
+
// path-check: the name of a health dependency, not a topic.
|
|
1277
1268
|
name: `collections.${collections.join("+")}`,
|
|
1278
1269
|
collections,
|
|
1279
1270
|
storage,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The metadata of an unbounded stream response (roadmap R15). `ProcedureServer` writes it, and the
|
|
3
|
+
* HTTP server transport reads it. This module has no import, so a transport can use it without an
|
|
4
|
+
* import of the server (invariant I9, roadmap CLI-83).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The metadata key of a stream response whose procedure declares `stream: { unbounded: true }`.
|
|
9
|
+
* Its value is "unbounded".
|
|
10
|
+
*/
|
|
11
|
+
export const STREAM_METADATA_KEY = "stream";
|
|
12
|
+
|
|
13
|
+
/** True when a stream response can run without an end (roadmap R15). */
|
|
14
|
+
export function isUnboundedStream(response: { metadata?: Record<string, unknown> | undefined }): boolean {
|
|
15
|
+
return response.metadata?.[STREAM_METADATA_KEY] === "unbounded";
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry point "@procedures/client/testing" (roadmap CLI-85): the transport contract, for the
|
|
3
|
+
* test files of each face of the procedures server. A face in another package, or the server of
|
|
4
|
+
* an app, runs the same cases as the faces of this package. `WIRE-PROTOCOL.md` is the contract.
|
|
5
|
+
*
|
|
6
|
+
* It imports no test runner: the caller gives the functions of its runner (`ContractRunner`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
CONTRACT_COLLECTION,
|
|
11
|
+
CONTRACT_PATHS,
|
|
12
|
+
CONTRACT_PRINCIPAL,
|
|
13
|
+
CONTRACT_TOKEN,
|
|
14
|
+
clientCaller,
|
|
15
|
+
contractDescription,
|
|
16
|
+
describeTransportContract,
|
|
17
|
+
} from "./transport-contract.js";
|
|
18
|
+
export type {
|
|
19
|
+
ContractCallOptions,
|
|
20
|
+
ContractCaller,
|
|
21
|
+
ContractCapabilities,
|
|
22
|
+
ContractCase,
|
|
23
|
+
ContractExpectation,
|
|
24
|
+
ContractFace,
|
|
25
|
+
ContractRunner,
|
|
26
|
+
ContractServer,
|
|
27
|
+
ContractSpec,
|
|
28
|
+
} from "./transport-contract.js";
|
|
29
|
+
export { mcpCaller } from "./mcp-caller.js";
|
|
30
|
+
export type { McpClientLike } from "./mcp-caller.js";
|
|
31
|
+
export { LogStorage } from "./log-storage.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection storage of a Map with a change log (`v:<number>`), for the tests (CLI-85). The
|
|
3
|
+
* transport contract serves its collection from it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CollectionStorage, StorageChanges } from "../procedures/collection/storage.js";
|
|
7
|
+
|
|
8
|
+
export class LogStorage implements CollectionStorage<unknown> {
|
|
9
|
+
readonly data: Map<string, unknown> = new Map();
|
|
10
|
+
private seq = 0;
|
|
11
|
+
private readonly changedAt = new Map<string, number>();
|
|
12
|
+
private readonly deletedAt = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
/** The items are in the storage before the first version: a delta does not have them. */
|
|
15
|
+
constructor(items: Record<string, unknown> = {}) {
|
|
16
|
+
for (const [id, value] of Object.entries(items)) this.data.set(id, value);
|
|
17
|
+
}
|
|
18
|
+
async get(id: string): Promise<unknown> {
|
|
19
|
+
return this.data.get(id);
|
|
20
|
+
}
|
|
21
|
+
async getAll(): Promise<unknown[]> {
|
|
22
|
+
return [...this.data.values()];
|
|
23
|
+
}
|
|
24
|
+
async getEntries(): Promise<Array<[string, unknown]>> {
|
|
25
|
+
return [...this.data.entries()];
|
|
26
|
+
}
|
|
27
|
+
async getChangesSince(since?: string): Promise<StorageChanges<unknown>> {
|
|
28
|
+
const version = `v:${this.seq}`;
|
|
29
|
+
if (since === undefined) return { version, full: true, changed: [...this.data.entries()], deleted: [] };
|
|
30
|
+
const from = Number(since.slice(2));
|
|
31
|
+
const changed = [...this.changedAt].filter(([, at]) => at > from).map(([id]): [string, unknown] => [id, this.data.get(id)]);
|
|
32
|
+
const deleted = [...this.deletedAt].filter(([, at]) => at > from).map(([id]) => id);
|
|
33
|
+
return { version, full: false, changed, deleted };
|
|
34
|
+
}
|
|
35
|
+
async find(predicate: (item: unknown) => boolean): Promise<unknown[]> {
|
|
36
|
+
return [...this.data.values()].filter(predicate);
|
|
37
|
+
}
|
|
38
|
+
async has(id: string): Promise<boolean> {
|
|
39
|
+
return this.data.has(id);
|
|
40
|
+
}
|
|
41
|
+
async size(): Promise<number> {
|
|
42
|
+
return this.data.size;
|
|
43
|
+
}
|
|
44
|
+
async set(id: string, value: unknown): Promise<void> {
|
|
45
|
+
this.data.set(id, value);
|
|
46
|
+
this.changedAt.set(id, ++this.seq);
|
|
47
|
+
this.deletedAt.delete(id);
|
|
48
|
+
}
|
|
49
|
+
async delete(id: string): Promise<boolean> {
|
|
50
|
+
const existed = this.data.delete(id);
|
|
51
|
+
if (existed) {
|
|
52
|
+
this.deletedAt.set(id, ++this.seq);
|
|
53
|
+
this.changedAt.delete(id);
|
|
54
|
+
}
|
|
55
|
+
return existed;
|
|
56
|
+
}
|
|
57
|
+
async clear(): Promise<void> {
|
|
58
|
+
for (const id of [...this.data.keys()]) await this.delete(id);
|
|
59
|
+
}
|
|
60
|
+
async setBatch(items: Array<[string, unknown]>): Promise<void> {
|
|
61
|
+
for (const [id, value] of items) await this.set(id, value);
|
|
62
|
+
}
|
|
63
|
+
async deleteBatch(ids: string[]): Promise<number> {
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const id of ids) if (await this.delete(id)) count++;
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
async getBatch(ids: string[]): Promise<Map<string, unknown>> {
|
|
69
|
+
return new Map(ids.filter((id) => this.data.has(id)).map((id): [string, unknown] => [id, this.data.get(id)]));
|
|
70
|
+
}
|
|
71
|
+
async close(): Promise<void> {}
|
|
72
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A caller of the transport contract over an MCP client (CLI-85). The MCP faces of `client-mcp`
|
|
3
|
+
* and of a host in `client-server` use it. It has no import of the MCP SDK: the face gives the
|
|
4
|
+
* client, so the caller works with the copy of the SDK of each package.
|
|
5
|
+
*
|
|
6
|
+
* An MCP tool call is one result. A stream comes back as one result with a text block for each
|
|
7
|
+
* item. An error result has `isError`, its code and its flag in `_meta`, and its message in the
|
|
8
|
+
* last text block.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pathToKey } from "../index.js";
|
|
12
|
+
import { contractDescription, type ContractCaller } from "./transport-contract.js";
|
|
13
|
+
|
|
14
|
+
/** The parts of an MCP client of the SDK that the caller uses. */
|
|
15
|
+
export interface McpClientLike {
|
|
16
|
+
listTools(): Promise<{ tools: Array<{ name: string; description?: string | undefined }> }>;
|
|
17
|
+
callTool(params: { name: string; arguments?: Record<string, unknown> }, resultSchema?: undefined, options?: { signal?: AbortSignal }): Promise<unknown>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ToolResult = { content?: Array<{ type: string; text?: string }>; isError?: boolean; _meta?: Record<string, unknown> };
|
|
22
|
+
|
|
23
|
+
/** The value of a text block: JSON, or the text itself. */
|
|
24
|
+
function valueOf(text: string | undefined): unknown {
|
|
25
|
+
if (text === undefined) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(text);
|
|
28
|
+
} catch {
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The error of an error result: the code and the flag of `_meta`, and the message of the last text
|
|
35
|
+
* block. The blocks of a stream that fails are its items, then the message of the error.
|
|
36
|
+
*/
|
|
37
|
+
function errorOf(result: ToolResult): Error {
|
|
38
|
+
const meta = result._meta ?? {};
|
|
39
|
+
return Object.assign(new Error(result.content?.at(-1)?.text ?? ""), { code: meta["code"], retryable: meta["retryable"] === true, metadata: meta });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The error of a refusal of the HTTP endpoint, before the tool call: the SDK puts the JSON-RPC
|
|
44
|
+
* body of the refusal in its message. Its `data.code` is the code of the refusal.
|
|
45
|
+
*/
|
|
46
|
+
function refusalOf(error: unknown): unknown {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
const start = message.indexOf("{");
|
|
49
|
+
if (start < 0) return error;
|
|
50
|
+
try {
|
|
51
|
+
const body = JSON.parse(message.slice(start)) as { error?: { message?: string; data?: { code?: string; retryable?: boolean } } };
|
|
52
|
+
const code = body.error?.data?.code;
|
|
53
|
+
if (code === undefined) return error;
|
|
54
|
+
return Object.assign(new Error(body.error?.message ?? message), { code, retryable: body.error?.data?.retryable === true });
|
|
55
|
+
} catch {
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A caller of an MCP client. It finds the tool of a path by the description of the procedure, so
|
|
62
|
+
* it does not depend on the form of a tool name. The client connects at its first call, so a
|
|
63
|
+
* refusal of the connection is the error of that call.
|
|
64
|
+
*/
|
|
65
|
+
export function mcpCaller(open: () => Promise<McpClientLike>): ContractCaller {
|
|
66
|
+
let client: Promise<{ mcp: McpClientLike; names: Map<string, string> }> | undefined;
|
|
67
|
+
const connect = () =>
|
|
68
|
+
(client ??= (async () => {
|
|
69
|
+
const mcp = await open();
|
|
70
|
+
const { tools } = await mcp.listTools();
|
|
71
|
+
return { mcp, names: new Map(tools.map((tool) => [tool.description ?? "", tool.name])) };
|
|
72
|
+
})());
|
|
73
|
+
const call = async (path: readonly string[], input: unknown, signal?: AbortSignal): Promise<ToolResult> => {
|
|
74
|
+
try {
|
|
75
|
+
const { mcp, names } = await connect();
|
|
76
|
+
const name = names.get(contractDescription(path)) ?? pathToKey(path);
|
|
77
|
+
return (await mcp.callTool({ name, arguments: input as Record<string, unknown> }, undefined, signal ? { signal } : {})) as ToolResult;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw refusalOf(error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
async call(path, input, options) {
|
|
84
|
+
const result = await call(path, input, options?.signal);
|
|
85
|
+
if (result.isError) throw errorOf(result);
|
|
86
|
+
return valueOf(result.content?.[0]?.text);
|
|
87
|
+
},
|
|
88
|
+
async *stream(path, input, options) {
|
|
89
|
+
const result = await call(path, input, options?.signal);
|
|
90
|
+
if (result.isError) throw errorOf(result);
|
|
91
|
+
// A stream without items gives one block "(no items)" for the reader of the result
|
|
92
|
+
const blocks = result.content ?? [];
|
|
93
|
+
if (blocks.length === 1 && blocks[0]!.text === "(no items)") return;
|
|
94
|
+
for (const block of blocks) yield valueOf(block.text);
|
|
95
|
+
},
|
|
96
|
+
async close() {
|
|
97
|
+
if (client) await (await client.catch(() => undefined))?.mcp.close();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|