@emseepea/create-tool-server 0.0.14 → 0.0.20

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,8 +4,8 @@ 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
10
  must call a public web service. [Compare all templates](https://emseepea.github.io/emseepea/examples/).
11
11
 
@@ -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
 
@@ -49,6 +74,13 @@ npm start
49
74
  The endpoint is `http://127.0.0.1:3000/mcp` and the health check is
50
75
  `http://127.0.0.1:3000/healthz`.
51
76
 
77
+ ## Add Feedback
78
+
79
+ Install `@emseepea/feedback` when this server needs a detailed one-way
80
+ observation or a durable support conversation. Pass its tools through the
81
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
82
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
83
+
52
84
  ## Check This Example
53
85
 
54
86
  [Ordinary tests](test/) live in `test/`.
@@ -67,4 +99,9 @@ Check that Claude chooses the variety tool and understands its result:
67
99
  npm run test:llm
68
100
  ```
69
101
 
102
+ The semantic suite also checks that protected discovery offers the tool to a
103
+ permitted principal and does not offer it to a principal without permission.
104
+ Those two authorization cases assert native tool selection only, so they do not
105
+ pay for unrelated answer-meaning judgments.
106
+
70
107
  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
 
@@ -30,6 +55,13 @@ npm start
30
55
  The endpoint is `http://127.0.0.1:3000/mcp` and the health check is
31
56
  `http://127.0.0.1:3000/healthz`.
32
57
 
58
+ ## Add Feedback
59
+
60
+ Install `@emseepea/feedback` when this server needs a detailed one-way
61
+ observation or a durable support conversation. Pass its tools through the
62
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
63
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
64
+
33
65
  ## Check This Example
34
66
 
35
67
  [Ordinary tests](test/) live in `test/`.
@@ -48,4 +80,9 @@ Check that Claude chooses the variety tool and understands its result:
48
80
  npm run test:llm
49
81
  ```
50
82
 
83
+ The semantic suite also checks that protected discovery offers the tool to a
84
+ permitted principal and does not offer it to a principal without permission.
85
+ Those two authorization cases assert native tool selection only, so they do not
86
+ pay for unrelated answer-meaning judgments.
87
+
51
88
  If Claude is not already signed in, run `claude auth login` first.
@@ -1,14 +1,24 @@
1
1
  import test from "node:test";
2
2
  import {
3
+ assertNoToolCalls,
4
+ assertNoNegativeFeedback,
3
5
  assertResponseContains,
4
6
  assertResponseMeaning,
5
- assertToolCalls,
7
+ assertToolCallsWithOptionalFeedback,
6
8
  createConversation,
7
9
  } from "@emseepea/testing/semantic";
8
10
 
11
+ const protectedServer = new URL("./protected-server.mjs", import.meta.url);
12
+ const server = new URL(import.meta.resolve("@emseepea/feedback/testing-server"));
13
+ const environment = {
14
+ EMSEEPEA_EVAL_APP_MODULE: new URL("../dist/app.js", import.meta.url).href,
15
+ EMSEEPEA_EVAL_APP_FACTORY: "createToolServer",
16
+ };
17
+
9
18
  test("looks up pea varieties and compares a follow-up", async (t) => {
10
19
  const chat = await createConversation(t, {
11
- server: new URL("../dist/server.js", import.meta.url),
20
+ server,
21
+ environment,
12
22
  });
13
23
 
14
24
  // The first turn's flexible prose needs the one model judge. The follow-up's
@@ -17,7 +27,7 @@ test("looks up pea varieties and compares a follow-up", async (t) => {
17
27
  "Describe the pea type, growth habit, maturity time, and traits of Highland Snap.",
18
28
  );
19
29
 
20
- assertToolCalls(highland, [{
30
+ await assertToolCallsWithOptionalFeedback(highland, [{
21
31
  name: "get-pea-variety",
22
32
  arguments: { name: "Highland Snap" },
23
33
  }]);
@@ -32,9 +42,50 @@ test("looks up pea varieties and compares a follow-up", async (t) => {
32
42
  const comparison = await chat.send(
33
43
  "Compare that with Harbour Gem. Include its pea type, growth habit, and maturity time.",
34
44
  );
35
- assertToolCalls(comparison, [{
45
+ await assertToolCallsWithOptionalFeedback(comparison, [{
36
46
  name: "get-pea-variety",
37
47
  arguments: { name: "Harbour Gem" },
38
48
  }]);
39
49
  assertResponseContains(comparison, "Harbour Gem");
50
+ assertNoNegativeFeedback(highland, comparison);
51
+ });
52
+
53
+ test("protected discovery offers a permitted tool", async (t) => {
54
+ const chat = await createConversation(t, {
55
+ server: protectedServer,
56
+ authToken: "test-token",
57
+ environment: { TEST_PERMISSIONS: "varieties:read,feedback:write" },
58
+ });
59
+
60
+ // This one answer check confirms that protected discovery still supports a
61
+ // useful answer, while keeping the authorization case to one short turn.
62
+ const answer = await chat.send("What type of pea is Highland Snap?");
63
+ await assertToolCallsWithOptionalFeedback(answer, [{
64
+ name: "get-pea-variety",
65
+ arguments: { name: "Highland Snap" },
66
+ }]);
67
+ await assertResponseMeaning(answer, {
68
+ expected: "The response says that Highland Snap is a snap pea.",
69
+ });
70
+ assertNoNegativeFeedback(answer);
71
+ });
72
+
73
+ test("protected discovery does not offer a hidden tool", async (t) => {
74
+ const chat = await createConversation(t, {
75
+ server: protectedServer,
76
+ authToken: "test-token",
77
+ environment: { TEST_PERMISSIONS: "other:read" },
78
+ });
79
+
80
+ // Ask about the visible catalogue itself so the response tests discovery,
81
+ // rather than whether the model can guess an answer from general knowledge.
82
+ const answer = await chat.send(
83
+ "Do you have an available tool that can look up Highland Snap?",
84
+ );
85
+ assertNoToolCalls(answer);
86
+ await assertResponseMeaning(answer, {
87
+ expected:
88
+ "The response explains that it has no available tool for looking up Highland Snap.",
89
+ });
90
+ assertNoNegativeFeedback(answer);
40
91
  });
@@ -0,0 +1,28 @@
1
+ import { serveEmseepea } from "@emseepea/server";
2
+ import { insecureTestAuthentication } from "@emseepea/testing";
3
+ import { defineFeedbackSubmission } from "@emseepea/feedback";
4
+ import { createToolServer } from "../dist/app.js";
5
+
6
+ const permissions = process.env.TEST_PERMISSIONS?.split(",").filter(Boolean) ?? [];
7
+ const feedback = defineFeedbackSubmission({
8
+ access: "protected",
9
+ requiredScopes: ["feedback:write"],
10
+ backend: { submit: () => ({ id: crypto.randomUUID(), recordedAt: new Date().toISOString() }) },
11
+ });
12
+ const app = await createToolServer({
13
+ access: { access: "protected", requiredScopes: ["varieties:read"] },
14
+ authentication: insecureTestAuthentication(permissions),
15
+ additionalTools: [feedback],
16
+ });
17
+ const running = await serveEmseepea(app, {
18
+ port: Number.parseInt(process.env.PORT ?? "3000", 10),
19
+ });
20
+
21
+ console.log(`Protected tool eval server listening at ${running.url}`);
22
+
23
+ async function shutdown() {
24
+ await running.close();
25
+ }
26
+
27
+ process.once("SIGINT", () => void shutdown());
28
+ process.once("SIGTERM", () => void shutdown());
@@ -14,7 +14,8 @@
14
14
  "lint": "oxlint src test eval"
15
15
  },
16
16
  "devDependencies": {
17
- "@emseepea/testing": "0.5.3",
17
+ "@emseepea/feedback": "0.2.0",
18
+ "@emseepea/testing": "0.9.3",
18
19
  "@types/node": "24.13.3",
19
20
  "typescript": "6.0.3",
20
21
  "oxlint": "1.80.0"
@@ -24,7 +25,7 @@
24
25
  },
25
26
  "private": true,
26
27
  "dependencies": {
27
- "@emseepea/server": "0.3.3",
28
+ "@emseepea/server": "0.6.1",
28
29
  "zod": "4.4.3"
29
30
  }
30
31
  }
@@ -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.14",
3
+ "version": "0.0.20",
4
4
  "description": "Create an Em See Pea server with one public tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,9 +18,10 @@
18
18
  "prepack": "npm run build:initializer"
19
19
  },
20
20
  "devDependencies": {
21
- "@emseepea/server": "0.3.3",
21
+ "@emseepea/feedback": "0.2.0",
22
+ "@emseepea/server": "0.6.1",
22
23
  "zod": "4.4.3",
23
- "@emseepea/testing": "0.5.3",
24
+ "@emseepea/testing": "0.9.3",
24
25
  "@types/node": "24.13.3",
25
26
  "typescript": "6.0.3",
26
27
  "oxlint": "1.80.0"