@emseepea/create-progress-streaming-server 0.0.14 → 0.0.19

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
@@ -46,7 +46,8 @@ bodies, arguments, results, tokens, provider claims, or raw errors. See the
46
46
  Choose this example when a tool takes long enough that people benefit from
47
47
  seeing progress before the final answer.
48
48
 
49
- The public tool reports progress during its request. A client can ask for
49
+ The tool reports progress during its request. It can use the public access shown
50
+ in the template or the protected access described above. A client can ask for
50
51
  server-sent events (SSE), which carry progress over the same `POST` request.
51
52
  Without that request, the tool returns one JSON response when it finishes.
52
53
 
@@ -63,11 +64,18 @@ npm start
63
64
  The server listens on `http://127.0.0.1:3000/mcp` by default. Set `PORT` to
64
65
  choose another port. This example starts locally and does not configure a proxy.
65
66
 
66
- To adapt it for a public server, see
67
+ To adapt it for a deployed server, see
67
68
  [Use Progress Behind a Proxy](https://github.com/emseepea/emseepea/blob/main/packages/framework/README.md#use-progress-behind-a-proxy).
68
69
  It does not add saved sessions, replay, subscriptions, or recovery after
69
70
  reconnecting.
70
71
 
72
+ ## Add Feedback
73
+
74
+ Install `@emseepea/feedback` when this server needs a detailed one-way
75
+ observation or a durable support conversation. Pass its tools through the
76
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
77
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
78
+
71
79
  ## Check This Example
72
80
 
73
81
  [Ordinary tests](test/) live in `test/`.
@@ -27,7 +27,8 @@ bodies, arguments, results, tokens, provider claims, or raw errors. See the
27
27
  Choose this example when a tool takes long enough that people benefit from
28
28
  seeing progress before the final answer.
29
29
 
30
- The public tool reports progress during its request. A client can ask for
30
+ The tool reports progress during its request. It can use the public access shown
31
+ in the template or the protected access described above. A client can ask for
31
32
  server-sent events (SSE), which carry progress over the same `POST` request.
32
33
  Without that request, the tool returns one JSON response when it finishes.
33
34
 
@@ -44,11 +45,18 @@ npm start
44
45
  The server listens on `http://127.0.0.1:3000/mcp` by default. Set `PORT` to
45
46
  choose another port. This example starts locally and does not configure a proxy.
46
47
 
47
- To adapt it for a public server, see
48
+ To adapt it for a deployed server, see
48
49
  [Use Progress Behind a Proxy](https://github.com/emseepea/emseepea/blob/main/packages/framework/README.md#use-progress-behind-a-proxy).
49
50
  It does not add saved sessions, replay, subscriptions, or recovery after
50
51
  reconnecting.
51
52
 
53
+ ## Add Feedback
54
+
55
+ Install `@emseepea/feedback` when this server needs a detailed one-way
56
+ observation or a durable support conversation. Pass its tools through the
57
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
58
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
59
+
52
60
  ## Check This Example
53
61
 
54
62
  [Ordinary tests](test/) live in `test/`.
@@ -1,15 +1,23 @@
1
1
  import test from "node:test";
2
2
  import {
3
3
  assertNoToolCalls,
4
+ assertNoNegativeFeedback,
4
5
  assertResponseContains,
5
6
  assertResponseMeaning,
6
- assertToolCalls,
7
+ assertToolCallsWithOptionalFeedback,
7
8
  createConversation,
8
9
  } from "@emseepea/testing/semantic";
9
10
 
11
+ const server = new URL(import.meta.resolve("@emseepea/feedback/testing-server"));
12
+ const environment = {
13
+ EMSEEPEA_EVAL_APP_MODULE: new URL("../dist/app.js", import.meta.url).href,
14
+ EMSEEPEA_EVAL_APP_FACTORY: "createProgressStreamingServer",
15
+ };
16
+
10
17
  test("keeps progress stages distinct from the completed result", async (t) => {
11
18
  const chat = await createConversation(t, {
12
- server: new URL("../dist/server.js", import.meta.url),
19
+ server,
20
+ environment,
13
21
  });
14
22
 
15
23
  // The judge checks the important progress-versus-result distinction once.
@@ -18,7 +26,7 @@ test("keeps progress stages distinct from the completed result", async (t) => {
18
26
  "Run the sample-tray pea germination trial. List its progress stages and final result.",
19
27
  );
20
28
 
21
- assertToolCalls(response, [{
29
+ await assertToolCallsWithOptionalFeedback(response, [{
22
30
  name: "run-germination-trial",
23
31
  arguments: { tray: "sample-tray" },
24
32
  }]);
@@ -34,4 +42,5 @@ test("keeps progress stages distinct from the completed result", async (t) => {
34
42
  );
35
43
  assertNoToolCalls(followUp);
36
44
  assertResponseContains(followUp, "sow");
45
+ assertNoNegativeFeedback(response, followUp);
37
46
  });
@@ -14,7 +14,9 @@
14
14
  "lint": "oxlint src test eval"
15
15
  },
16
16
  "devDependencies": {
17
- "@emseepea/testing": "0.6.1",
17
+ "@emseepea/feedback": "0.2.1",
18
+ "@emseepea/testing": "0.9.4",
19
+ "@modelcontextprotocol/client": "2.0.0",
18
20
  "@types/node": "24.13.3",
19
21
  "typescript": "6.0.3",
20
22
  "oxlint": "1.80.0"
@@ -24,7 +26,7 @@
24
26
  },
25
27
  "private": true,
26
28
  "dependencies": {
27
- "@emseepea/server": "0.4.0",
29
+ "@emseepea/server": "0.7.0",
28
30
  "zod": "4.4.3"
29
31
  }
30
32
  }
@@ -1,6 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
+ import { once } from "node:events";
3
+ import { createServer, request } from "node:http";
2
4
  import test from "node:test";
3
5
 
6
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
7
+ import { serveEmseepea } from "@emseepea/server";
4
8
  import {
5
9
  insecureTestAuthentication,
6
10
  startEmseepea,
@@ -44,10 +48,127 @@ test("the same template composes protected access and observability", async (t)
44
48
  observability: [{ id: "test-log", emit: (event) => events.push(event) }],
45
49
  }));
46
50
  const client = await running.connect("test-token");
47
- const result = await client.callTool({
48
- name: "run-germination-trial",
49
- arguments: { tray: "sample-tray" },
50
- });
51
+ const progress = [];
52
+ const result = await client.callTool(
53
+ { name: "run-germination-trial", arguments: { tray: "sample-tray" } },
54
+ { onprogress: (update) => progress.push(update) },
55
+ );
56
+ assert.deepEqual(progress.map(({ message }) => message), ["soak", "sow", "sprout"]);
51
57
  assert.equal(result.structuredContent.status, "complete");
52
58
  assert.ok(events.some(({ capability }) => capability === "run-germination-trial"));
53
59
  });
60
+
61
+ test("protected progress crosses a trusted proxy through official and raw clients", async (t) => {
62
+ const permissions = ["trials:run"];
63
+ const backend = await serveEmseepea(await createProgressStreamingServer({
64
+ access: { access: "protected", requiredScopes: permissions },
65
+ authentication: insecureTestAuthentication(permissions, "public"),
66
+ deployment: {
67
+ mode: "production-behind-proxy",
68
+ trustedProxyAddresses: ["127.0.0.1"],
69
+ allowedAuthorities: ["test.example"],
70
+ allowedOrigins: ["https://test.example"],
71
+ rateLimit: { maxRequests: 20, windowMs: 1_000, maxClients: 1 },
72
+ },
73
+ }), { port: 0 });
74
+ const proxy = await startTrustedProxy(backend.url);
75
+ t.after(async () => {
76
+ proxy.closeAllConnections();
77
+ await new Promise((resolve) => proxy.close(resolve));
78
+ await backend.close();
79
+ });
80
+ const url = new URL(`http://127.0.0.1:${proxy.address().port}/mcp`);
81
+
82
+ const client = new Client(
83
+ { name: "packed-progress-test", version: "0.0.0" },
84
+ { versionNegotiation: { mode: { pin: "2026-07-28" } } },
85
+ );
86
+ try {
87
+ await client.connect(new StreamableHTTPClientTransport(url, {
88
+ requestInit: { headers: {
89
+ Accept: "application/json, text/event-stream",
90
+ Authorization: "Bearer official-token",
91
+ } },
92
+ }));
93
+ const progress = [];
94
+ const result = await client.callTool(
95
+ { name: "run-germination-trial", arguments: { tray: "sample-tray" } },
96
+ { onprogress: (update) => progress.push(update) },
97
+ );
98
+ assert.ok(proxy.requestBodies.some((body) => body.includes('"progressToken"')));
99
+ assert.equal(proxy.responseTypes.at(-1), "text/event-stream");
100
+ assert.deepEqual(progress.map(({ message }) => message), ["soak", "sow", "sprout"]);
101
+ assert.equal(result.structuredContent.tray, "sample-tray");
102
+ } finally {
103
+ await client.close();
104
+ }
105
+
106
+ const response = await fetch(url, rawCall("raw-token"));
107
+ assert.equal(response.status, 200);
108
+ assert.match(response.headers.get("content-type"), /^text\/event-stream/);
109
+ const messages = (await response.text()).trim().split("\n\n").map((frame) =>
110
+ JSON.parse(frame.slice(frame.indexOf("data: ") + 6)));
111
+ assert.deepEqual(messages.slice(0, -1).map(({ params }) => params.message), ["soak", "sow", "sprout"]);
112
+ assert.equal(messages.at(-1).result.structuredContent.tray, "sample-tray");
113
+ assert.doesNotMatch(JSON.stringify(messages), /raw-token/);
114
+ });
115
+
116
+ async function startTrustedProxy(backendUrl) {
117
+ const requestBodies = [];
118
+ const responseTypes = [];
119
+ const proxy = createServer((incoming, outgoing) => {
120
+ const chunks = [];
121
+ incoming.on("data", (chunk) => chunks.push(chunk));
122
+ incoming.on("end", () => requestBodies.push(Buffer.concat(chunks).toString("utf8")));
123
+ const upstream = request(new URL(incoming.url, backendUrl), {
124
+ method: incoming.method,
125
+ headers: {
126
+ ...incoming.headers,
127
+ host: "test.example",
128
+ "x-forwarded-for": "192.0.2.1",
129
+ "x-forwarded-proto": "https",
130
+ },
131
+ });
132
+ upstream.on("response", (response) => {
133
+ responseTypes.push(response.headers["content-type"]);
134
+ outgoing.writeHead(response.statusCode, response.headers);
135
+ response.pipe(outgoing);
136
+ });
137
+ upstream.on("error", () => outgoing.destroy());
138
+ incoming.pipe(upstream);
139
+ });
140
+ proxy.listen(0, "127.0.0.1");
141
+ await once(proxy, "listening");
142
+ proxy.requestBodies = requestBodies;
143
+ proxy.responseTypes = responseTypes;
144
+ return proxy;
145
+ }
146
+
147
+ function rawCall(token) {
148
+ return {
149
+ method: "POST",
150
+ headers: {
151
+ Accept: "application/json, text/event-stream",
152
+ Authorization: `Bearer ${token}`,
153
+ "Content-Type": "application/json",
154
+ "MCP-Protocol-Version": "2026-07-28",
155
+ "Mcp-Method": "tools/call",
156
+ "Mcp-Name": "run-germination-trial",
157
+ },
158
+ body: JSON.stringify({
159
+ jsonrpc: "2.0",
160
+ id: "raw-call",
161
+ method: "tools/call",
162
+ params: {
163
+ name: "run-germination-trial",
164
+ arguments: { tray: "sample-tray" },
165
+ _meta: {
166
+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
167
+ "io.modelcontextprotocol/clientInfo": { name: "raw-progress-test", version: "0.0.0" },
168
+ "io.modelcontextprotocol/clientCapabilities": {},
169
+ progressToken: "raw-progress",
170
+ },
171
+ },
172
+ }),
173
+ };
174
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-progress-streaming-server",
3
- "version": "0.0.14",
3
+ "version": "0.0.19",
4
4
  "description": "Create an Em See Pea server that streams tool progress.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,9 +18,11 @@
18
18
  "prepack": "npm run build:initializer"
19
19
  },
20
20
  "devDependencies": {
21
- "@emseepea/server": "0.4.0",
21
+ "@emseepea/feedback": "0.2.1",
22
+ "@emseepea/server": "0.7.0",
22
23
  "zod": "4.4.3",
23
- "@emseepea/testing": "0.6.1",
24
+ "@emseepea/testing": "0.9.4",
25
+ "@modelcontextprotocol/client": "2.0.0",
24
26
  "@types/node": "24.13.3",
25
27
  "typescript": "6.0.3",
26
28
  "oxlint": "1.80.0"