@emseepea/create-tool-server 0.0.13 → 0.0.16
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 +35 -5
- package/initializer-dist/template/README.md +33 -3
- package/initializer-dist/template/eval/meaning.test.mjs +41 -0
- package/initializer-dist/template/eval/protected-server.mjs +21 -0
- package/initializer-dist/template/package.json +2 -2
- package/initializer-dist/template/src/app.ts +21 -0
- package/initializer-dist/template/src/capabilities/tool.get-pea-variety.ts +4 -4
- package/initializer-dist/template/src/server.ts +3 -9
- package/initializer-dist/template/test/server.test.mjs +24 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ This directory is both the maintained example and its public npm initializer.
|
|
|
4
4
|
|
|
5
5
|
## Use This Template
|
|
6
6
|
|
|
7
|
-
Use this template for the smallest server with one
|
|
8
|
-
|
|
7
|
+
Use this template for the smallest server with one read-only tool and no
|
|
8
|
+
external service, web page, progress stream, or shared storage.
|
|
9
9
|
Choose the [API-backed server](../api-backed-server/README.md) when the tool
|
|
10
|
-
must call a public web service. [Compare all
|
|
10
|
+
must call a public web service. [Compare all templates](https://emseepea.github.io/emseepea/examples/).
|
|
11
11
|
|
|
12
12
|
## Create a Project
|
|
13
13
|
|
|
@@ -17,6 +17,30 @@ npm init @emseepea/tool-server -- my-server
|
|
|
17
17
|
|
|
18
18
|
<!-- generated-project-readme -->
|
|
19
19
|
|
|
20
|
+
## Choose Open or Protected Access
|
|
21
|
+
|
|
22
|
+
Start open when the catalogue and operations are public.
|
|
23
|
+
|
|
24
|
+
To protect this template, pass both options to the app factory:
|
|
25
|
+
|
|
26
|
+
- `access: { access: "protected", requiredScopes: ["peas:read"] }`
|
|
27
|
+
- an `authentication` adapter
|
|
28
|
+
|
|
29
|
+
Keep `authentication.discovery` as `"public"` unless capability names or
|
|
30
|
+
schemas are sensitive. Use `"protected"` only when each principal should see a
|
|
31
|
+
permission-filtered catalogue. OAuth metadata remains public in both modes.
|
|
32
|
+
|
|
33
|
+
## Add Observability
|
|
34
|
+
|
|
35
|
+
The same factory accepts `observability`.
|
|
36
|
+
|
|
37
|
+
- Use `structuredLogging` for safe structured events.
|
|
38
|
+
- Use `openTelemetry` for traces and metrics.
|
|
39
|
+
|
|
40
|
+
Adapters receive only redacted framework events. They never receive request
|
|
41
|
+
bodies, arguments, results, tokens, provider claims, or raw errors. See the
|
|
42
|
+
[server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
|
|
43
|
+
|
|
20
44
|
## Your First Public Tool
|
|
21
45
|
|
|
22
46
|
Choose this example when you want the smallest working server with one public,
|
|
@@ -33,8 +57,9 @@ See Pea checks the input and result before returning them through the public
|
|
|
33
57
|
- checks for tool input and output
|
|
34
58
|
- local use on your computer only
|
|
35
59
|
|
|
36
|
-
It does not include
|
|
37
|
-
production network setup.
|
|
60
|
+
It does not include saved sessions, live progress, changes to data, or
|
|
61
|
+
production network setup. The protected-access test shows how to add an
|
|
62
|
+
application-supplied authentication adapter.
|
|
38
63
|
|
|
39
64
|
## Run
|
|
40
65
|
|
|
@@ -67,4 +92,9 @@ Check that Claude chooses the variety tool and understands its result:
|
|
|
67
92
|
npm run test:llm
|
|
68
93
|
```
|
|
69
94
|
|
|
95
|
+
The semantic suite also checks that protected discovery offers the tool to a
|
|
96
|
+
permitted principal and does not offer it to a principal without permission.
|
|
97
|
+
Those two authorization cases assert native tool selection only, so they do not
|
|
98
|
+
pay for unrelated answer-meaning judgments.
|
|
99
|
+
|
|
70
100
|
If Claude is not already signed in, run `claude auth login` first.
|
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Choose Open or Protected Access
|
|
2
|
+
|
|
3
|
+
Start open when the catalogue and operations are public.
|
|
4
|
+
|
|
5
|
+
To protect this template, pass both options to the app factory:
|
|
6
|
+
|
|
7
|
+
- `access: { access: "protected", requiredScopes: ["peas:read"] }`
|
|
8
|
+
- an `authentication` adapter
|
|
9
|
+
|
|
10
|
+
Keep `authentication.discovery` as `"public"` unless capability names or
|
|
11
|
+
schemas are sensitive. Use `"protected"` only when each principal should see a
|
|
12
|
+
permission-filtered catalogue. OAuth metadata remains public in both modes.
|
|
13
|
+
|
|
14
|
+
## Add Observability
|
|
15
|
+
|
|
16
|
+
The same factory accepts `observability`.
|
|
17
|
+
|
|
18
|
+
- Use `structuredLogging` for safe structured events.
|
|
19
|
+
- Use `openTelemetry` for traces and metrics.
|
|
20
|
+
|
|
21
|
+
Adapters receive only redacted framework events. They never receive request
|
|
22
|
+
bodies, arguments, results, tokens, provider claims, or raw errors. See the
|
|
23
|
+
[server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
|
|
24
|
+
|
|
25
|
+
## Your First Public Tool
|
|
2
26
|
|
|
3
27
|
Choose this example when you want the smallest working server with one public,
|
|
4
28
|
read-only tool and no web page.
|
|
@@ -14,8 +38,9 @@ See Pea checks the input and result before returning them through the public
|
|
|
14
38
|
- checks for tool input and output
|
|
15
39
|
- local use on your computer only
|
|
16
40
|
|
|
17
|
-
It does not include
|
|
18
|
-
production network setup.
|
|
41
|
+
It does not include saved sessions, live progress, changes to data, or
|
|
42
|
+
production network setup. The protected-access test shows how to add an
|
|
43
|
+
application-supplied authentication adapter.
|
|
19
44
|
|
|
20
45
|
## Run
|
|
21
46
|
|
|
@@ -48,4 +73,9 @@ Check that Claude chooses the variety tool and understands its result:
|
|
|
48
73
|
npm run test:llm
|
|
49
74
|
```
|
|
50
75
|
|
|
76
|
+
The semantic suite also checks that protected discovery offers the tool to a
|
|
77
|
+
permitted principal and does not offer it to a principal without permission.
|
|
78
|
+
Those two authorization cases assert native tool selection only, so they do not
|
|
79
|
+
pay for unrelated answer-meaning judgments.
|
|
80
|
+
|
|
51
81
|
If Claude is not already signed in, run `claude auth login` first.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import {
|
|
3
|
+
assertNoToolCalls,
|
|
3
4
|
assertResponseContains,
|
|
4
5
|
assertResponseMeaning,
|
|
5
6
|
assertToolCalls,
|
|
6
7
|
createConversation,
|
|
7
8
|
} from "@emseepea/testing/semantic";
|
|
8
9
|
|
|
10
|
+
const protectedServer = new URL("./protected-server.mjs", import.meta.url);
|
|
11
|
+
|
|
9
12
|
test("looks up pea varieties and compares a follow-up", async (t) => {
|
|
10
13
|
const chat = await createConversation(t, {
|
|
11
14
|
server: new URL("../dist/server.js", import.meta.url),
|
|
@@ -38,3 +41,41 @@ test("looks up pea varieties and compares a follow-up", async (t) => {
|
|
|
38
41
|
}]);
|
|
39
42
|
assertResponseContains(comparison, "Harbour Gem");
|
|
40
43
|
});
|
|
44
|
+
|
|
45
|
+
test("protected discovery offers a permitted tool", async (t) => {
|
|
46
|
+
const chat = await createConversation(t, {
|
|
47
|
+
server: protectedServer,
|
|
48
|
+
authToken: "test-token",
|
|
49
|
+
environment: { TEST_PERMISSIONS: "varieties:read" },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// This one answer check confirms that protected discovery still supports a
|
|
53
|
+
// useful answer, while keeping the authorization case to one short turn.
|
|
54
|
+
const answer = await chat.send("What type of pea is Highland Snap?");
|
|
55
|
+
assertToolCalls(answer, [{
|
|
56
|
+
name: "get-pea-variety",
|
|
57
|
+
arguments: { name: "Highland Snap" },
|
|
58
|
+
}]);
|
|
59
|
+
await assertResponseMeaning(answer, {
|
|
60
|
+
expected: "The response says that Highland Snap is a snap pea.",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("protected discovery does not offer a hidden tool", async (t) => {
|
|
65
|
+
const chat = await createConversation(t, {
|
|
66
|
+
server: protectedServer,
|
|
67
|
+
authToken: "test-token",
|
|
68
|
+
environment: { TEST_PERMISSIONS: "other:read" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Ask about the visible catalogue itself so the response tests discovery,
|
|
72
|
+
// rather than whether the model can guess an answer from general knowledge.
|
|
73
|
+
const answer = await chat.send(
|
|
74
|
+
"Do you have an available tool that can look up Highland Snap?",
|
|
75
|
+
);
|
|
76
|
+
assertNoToolCalls(answer);
|
|
77
|
+
await assertResponseMeaning(answer, {
|
|
78
|
+
expected:
|
|
79
|
+
"The response explains that it has no available tool for looking up Highland Snap.",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { serveEmseepea } from "@emseepea/server";
|
|
2
|
+
import { insecureTestAuthentication } from "@emseepea/testing";
|
|
3
|
+
import { createToolServer } from "../dist/app.js";
|
|
4
|
+
|
|
5
|
+
const permissions = process.env.TEST_PERMISSIONS?.split(",").filter(Boolean) ?? [];
|
|
6
|
+
const app = await createToolServer({
|
|
7
|
+
access: { access: "protected", requiredScopes: ["varieties:read"] },
|
|
8
|
+
authentication: insecureTestAuthentication(permissions),
|
|
9
|
+
});
|
|
10
|
+
const running = await serveEmseepea(app, {
|
|
11
|
+
port: Number.parseInt(process.env.PORT ?? "3000", 10),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log(`Protected tool eval server listening at ${running.url}`);
|
|
15
|
+
|
|
16
|
+
async function shutdown() {
|
|
17
|
+
await running.close();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
process.once("SIGINT", () => void shutdown());
|
|
21
|
+
process.once("SIGTERM", () => void shutdown());
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"lint": "oxlint src test eval"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@emseepea/testing": "0.
|
|
17
|
+
"@emseepea/testing": "0.6.1",
|
|
18
18
|
"@types/node": "24.13.3",
|
|
19
19
|
"typescript": "6.0.3",
|
|
20
20
|
"oxlint": "1.80.0"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"private": true,
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@emseepea/server": "0.
|
|
27
|
+
"@emseepea/server": "0.4.0",
|
|
28
28
|
"zod": "4.4.3"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmseepea,
|
|
3
|
+
discoverCapabilities,
|
|
4
|
+
type AccessPolicy,
|
|
5
|
+
type EmseepeaExtensions,
|
|
6
|
+
} from "@emseepea/server";
|
|
7
|
+
|
|
8
|
+
export interface ToolServerOptions extends EmseepeaExtensions {
|
|
9
|
+
readonly access?: AccessPolicy;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function createToolServer(options: ToolServerOptions = {}) {
|
|
13
|
+
const { access = { access: "public" }, ...extensions } = options;
|
|
14
|
+
return createEmseepea({
|
|
15
|
+
name: "emseepea-tool-server",
|
|
16
|
+
version: "0.0.0",
|
|
17
|
+
instructions: "Use get-pea-variety for information about a sample pea variety.",
|
|
18
|
+
...await discoverCapabilities(new URL("./capabilities/", import.meta.url), access),
|
|
19
|
+
...extensions,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
|
|
1
|
+
import { defineTool, type AccessPolicy, type CapabilityModuleFactory } from "@emseepea/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
const varietyNames = ["Harbour Gem", "Highland Snap"] as const;
|
|
@@ -29,12 +29,12 @@ const varieties: Record<(typeof varietyNames)[number], z.input<typeof outputSche
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
export default (() => defineTool({
|
|
32
|
+
export default ((access) => defineTool({
|
|
33
33
|
name: "get-pea-variety",
|
|
34
|
-
access
|
|
34
|
+
...access,
|
|
35
35
|
title: "Pea Variety Details",
|
|
36
36
|
description: "Get the type, growth habit, maturity time, and traits of a sample pea variety.",
|
|
37
37
|
inputSchema,
|
|
38
38
|
outputSchema,
|
|
39
39
|
handler: ({ name }) => ({ data: varieties[name] }),
|
|
40
|
-
})) satisfies CapabilityModuleFactory
|
|
40
|
+
})) satisfies CapabilityModuleFactory<AccessPolicy>;
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serveEmseepea } from "@emseepea/server";
|
|
2
|
+
import { createToolServer } from "./app.js";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
name: "emseepea-tool-server",
|
|
5
|
-
version: "0.0.0",
|
|
6
|
-
instructions: "Use get-pea-variety for information about a sample pea variety.",
|
|
7
|
-
...await discoverCapabilities(new URL("./capabilities/", import.meta.url)),
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const running = await serveEmseepea(handler, {
|
|
4
|
+
const running = await serveEmseepea(await createToolServer(), {
|
|
11
5
|
port: Number.parseInt(process.env.PORT ?? "3000", 10),
|
|
12
6
|
});
|
|
13
7
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
insecureTestAuthentication,
|
|
6
|
+
startEmseepea,
|
|
7
|
+
startMcpServer,
|
|
8
|
+
} from "@emseepea/testing";
|
|
9
|
+
import { createToolServer } from "../dist/app.js";
|
|
5
10
|
|
|
6
11
|
test("advertises and returns understandable pea variety details", async (t) => {
|
|
7
12
|
const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url));
|
|
@@ -31,3 +36,21 @@ test("advertises and returns understandable pea variety details", async (t) => {
|
|
|
31
36
|
});
|
|
32
37
|
assert.equal(result.content[0].text, JSON.stringify(result.structuredContent));
|
|
33
38
|
});
|
|
39
|
+
|
|
40
|
+
test("the same template composes protected access and observability", async (t) => {
|
|
41
|
+
const events = [];
|
|
42
|
+
const permissions = ["varieties:read"];
|
|
43
|
+
const running = await startEmseepea(t, await createToolServer({
|
|
44
|
+
access: { access: "protected", requiredScopes: permissions },
|
|
45
|
+
authentication: insecureTestAuthentication(permissions),
|
|
46
|
+
observability: [{ id: "test-log", emit: (event) => events.push(event) }],
|
|
47
|
+
}));
|
|
48
|
+
const client = await running.connect("test-token");
|
|
49
|
+
assert.deepEqual((await client.listTools()).tools.map(({ name }) => name), ["get-pea-variety"]);
|
|
50
|
+
const result = await client.callTool({
|
|
51
|
+
name: "get-pea-variety",
|
|
52
|
+
arguments: { name: "Harbour Gem" },
|
|
53
|
+
});
|
|
54
|
+
assert.equal(result.structuredContent.name, "Harbour Gem");
|
|
55
|
+
assert.ok(events.some(({ capability }) => capability === "get-pea-variety"));
|
|
56
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-tool-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Create an Em See Pea server with one public tool.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"prepack": "npm run build:initializer"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@emseepea/server": "0.
|
|
21
|
+
"@emseepea/server": "0.4.0",
|
|
22
22
|
"zod": "4.4.3",
|
|
23
|
-
"@emseepea/testing": "0.
|
|
23
|
+
"@emseepea/testing": "0.6.1",
|
|
24
24
|
"@types/node": "24.13.3",
|
|
25
25
|
"typescript": "6.0.3",
|
|
26
26
|
"oxlint": "1.80.0"
|