@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 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 public, read-only tool and
8
- no external service, sign-in, web page, progress stream, or shared storage.
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 eight templates](https://emseepea.github.io/emseepea/examples/).
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 sign-in, saved sessions, live progress, changes to data, or
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
- # Your First Public Tool
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 sign-in, saved sessions, live progress, changes to data, or
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.5.2",
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.3.3",
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: "public",
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 { createEmseepea, discoverCapabilities, serveEmseepea } from "@emseepea/server";
1
+ import { serveEmseepea } from "@emseepea/server";
2
+ import { createToolServer } from "./app.js";
2
3
 
3
- const handler = createEmseepea({
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 { startMcpServer } from "@emseepea/testing";
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.13",
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.3.3",
21
+ "@emseepea/server": "0.4.0",
22
22
  "zod": "4.4.3",
23
- "@emseepea/testing": "0.5.2",
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"