@emseepea/server 0.0.0 → 0.0.1
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 +321 -3
- package/package.json +43 -2
package/README.md
CHANGED
|
@@ -1,5 +1,323 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@emseepea/server`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@emseepea/server` is the Fastify-first package for building Model Context
|
|
4
|
+
Protocol (MCP) `2026-07-28` servers over Streamable HTTP.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
It checks data when requests enter and leave the server. It supports public
|
|
7
|
+
tools, tools that require sign-in, public resources and prompts, request time
|
|
8
|
+
limits, and bounded progress updates for local examples. Prompt arguments and
|
|
9
|
+
resource fields may also offer suggestions.
|
|
10
|
+
|
|
11
|
+
The package is pre-alpha. See the
|
|
12
|
+
[repository README](https://github.com/windyroad/emseepea#readme) for what the
|
|
13
|
+
package supports and what it does not support.
|
|
14
|
+
|
|
15
|
+
## Public Tool
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createEmseepea, defineMappedTool, defineTool, serveEmseepea } from "@emseepea/server";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
const beanDetails = z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
origin: z.string(),
|
|
24
|
+
variety: z.string(),
|
|
25
|
+
process: z.enum(["washed", "natural"]),
|
|
26
|
+
roast: z.enum(["light", "medium", "dark"]),
|
|
27
|
+
tastingNotes: z.array(z.string()),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const getBeanDetails = defineTool({
|
|
31
|
+
name: "get-bean-details",
|
|
32
|
+
access: "public",
|
|
33
|
+
description: "Get the origin, variety, process, roast, and tasting notes for one coffee.",
|
|
34
|
+
inputSchema: z.object({ name: z.string() }),
|
|
35
|
+
outputSchema: beanDetails,
|
|
36
|
+
handler: ({ name }) => {
|
|
37
|
+
const data = {
|
|
38
|
+
name,
|
|
39
|
+
origin: "Sample Highlands",
|
|
40
|
+
variety: "Bourbon",
|
|
41
|
+
process: "natural" as const,
|
|
42
|
+
roast: "medium" as const,
|
|
43
|
+
tastingNotes: ["berry", "cocoa"],
|
|
44
|
+
};
|
|
45
|
+
return { text: `${name} is a medium-roast natural Bourbon from Sample Highlands.`, data };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const app = createEmseepea({ name: "coffee-guide", version: "1.0.0", tools: [getBeanDetails] });
|
|
50
|
+
await serveEmseepea(app);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Mapped Backend Tool
|
|
54
|
+
|
|
55
|
+
`defineMappedTool` validates the public input, mapped backend command, backend
|
|
56
|
+
result, and public output. The adapter receives only its validated command, the
|
|
57
|
+
request cancellation signal, and the shared deadline.
|
|
58
|
+
|
|
59
|
+
Use `defineTool` instead when the backend already accepts the public input and
|
|
60
|
+
returns the public output. `defineMappedTool` is for a real contract boundary,
|
|
61
|
+
not an identity mapping.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const backendCommand = z.object({ search: z.string() });
|
|
65
|
+
const backendResult = z.object({
|
|
66
|
+
record: z.object({
|
|
67
|
+
name: z.string(),
|
|
68
|
+
country: z.string(),
|
|
69
|
+
variety: z.string(),
|
|
70
|
+
processCode: z.enum(["W", "N"]),
|
|
71
|
+
roastLevel: z.enum(["light", "medium", "dark"]),
|
|
72
|
+
flavourNotes: z.array(z.string()),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const getBeanDetails = defineMappedTool({
|
|
77
|
+
name: "get-bean-details",
|
|
78
|
+
access: "public",
|
|
79
|
+
description: "Get clear coffee details from the catalogue service.",
|
|
80
|
+
inputSchema: z.object({ name: z.string() }),
|
|
81
|
+
outputSchema: beanDetails,
|
|
82
|
+
backendInputSchema: backendCommand,
|
|
83
|
+
backendOutputSchema: backendResult,
|
|
84
|
+
mapInput: ({ name }) => ({ search: name }),
|
|
85
|
+
adapter: async (command, { signal }) => backend.findCoffee(command, { signal }),
|
|
86
|
+
mapOutput: ({ record }) => {
|
|
87
|
+
const data = {
|
|
88
|
+
name: record.name,
|
|
89
|
+
origin: record.country,
|
|
90
|
+
variety: record.variety,
|
|
91
|
+
process: record.processCode === "W" ? "washed" as const : "natural" as const,
|
|
92
|
+
roast: record.roastLevel,
|
|
93
|
+
tastingNotes: record.flavourNotes,
|
|
94
|
+
};
|
|
95
|
+
return { text: `${data.name} is a ${data.roast}-roast ${data.process} coffee from ${data.origin}.`, data };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For a read-only JSON API that does not require sign-in, use the narrow HTTP
|
|
101
|
+
client from `@emseepea/server/http`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { createJsonHttpClient } from "@emseepea/server/http";
|
|
105
|
+
|
|
106
|
+
const coffeeApi = createJsonHttpClient({ origin: "https://coffee.example" });
|
|
107
|
+
const result = await coffeeApi.get({
|
|
108
|
+
pathname: "/api/coffees",
|
|
109
|
+
searchParams: { q: "ethiopia", limit: "5" },
|
|
110
|
+
signal,
|
|
111
|
+
deadlineMs,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The client connects only to the configured HTTPS site. It blocks local and
|
|
116
|
+
private network addresses, redirects, compressed responses, oversized data,
|
|
117
|
+
and responses that are not JSON. It does not send credentials, retry requests,
|
|
118
|
+
cache data, accept custom headers, or change data. The result is `unknown` so a
|
|
119
|
+
mapped tool must check it with its backend output schema before using it.
|
|
120
|
+
|
|
121
|
+
A mapped tool may add an `isAvailable` function when its backend can be
|
|
122
|
+
temporarily unavailable. The check receives the same cancellation signal and
|
|
123
|
+
deadline as the adapter. It must be quick and must not change data.
|
|
124
|
+
|
|
125
|
+
An unavailable tool stays in `tools/list`, but calls to it return a generic
|
|
126
|
+
failure before mapping or backend work begins. Other tools and `/readyz` keep
|
|
127
|
+
working because `/readyz` reports whether the server process is ready, not
|
|
128
|
+
whether every application backend is available.
|
|
129
|
+
|
|
130
|
+
## Form Presentation
|
|
131
|
+
|
|
132
|
+
Use `defineElicitationView` to validate form display data and
|
|
133
|
+
`renderElicitationForm` to create a native HTML fragment. The host application
|
|
134
|
+
still owns its page, request handling, authorization, and effects.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { defineElicitationView, renderElicitationForm } from "@emseepea/server";
|
|
138
|
+
|
|
139
|
+
const view = defineElicitationView({
|
|
140
|
+
id: "bean-report",
|
|
141
|
+
heading: "Preview a bean report",
|
|
142
|
+
legend: "Report options",
|
|
143
|
+
submitLabel: "Preview report",
|
|
144
|
+
fields: [{
|
|
145
|
+
kind: "text",
|
|
146
|
+
id: "title",
|
|
147
|
+
name: "title",
|
|
148
|
+
label: "Report title",
|
|
149
|
+
required: true,
|
|
150
|
+
}],
|
|
151
|
+
state: { kind: "ready", focusTarget: "none" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const fragment = renderElicitationForm(view, { headingLevel: 2 });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The schema accepts presentation data only. It excludes raw HTML, destinations,
|
|
158
|
+
credentials, backend state, and permission to perform an action. Rendering a
|
|
159
|
+
form never means that an action was authorized or completed.
|
|
160
|
+
|
|
161
|
+
## Streaming Progress
|
|
162
|
+
|
|
163
|
+
Use `defineStreamingTool` only when a tool has meaningful progress to report.
|
|
164
|
+
It checks the input, progress updates, and final result. If the client asks for
|
|
165
|
+
progress, the official MCP library sends progress events over the same POST
|
|
166
|
+
request. Otherwise the call returns one JSON response.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { defineStreamingTool } from "@emseepea/server";
|
|
170
|
+
|
|
171
|
+
const roast = defineStreamingTool({
|
|
172
|
+
name: "roast-batch",
|
|
173
|
+
access: "public",
|
|
174
|
+
description: "Run one sample roast batch.",
|
|
175
|
+
inputSchema: z.object({ batch: z.string() }),
|
|
176
|
+
outputSchema: z.object({ status: z.literal("complete") }),
|
|
177
|
+
async handler(_input, { reportProgress, signal }) {
|
|
178
|
+
signal.throwIfAborted();
|
|
179
|
+
await reportProgress({ progress: 1, total: 1, message: "cool" });
|
|
180
|
+
return { text: "complete", data: { status: "complete" } };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Progress is strictly increasing and defaults to at most 32 small notification
|
|
186
|
+
payloads, measured before protocol encoding.
|
|
187
|
+
|
|
188
|
+
Set `maxProgressEvents` and `maxProgressEventBytes` on `createEmseepea` to
|
|
189
|
+
change those positive bounds. The reporter closes with the tool call.
|
|
190
|
+
Heartbeats are disabled. Invalid, oversized, late, or extra updates are
|
|
191
|
+
rejected.
|
|
192
|
+
|
|
193
|
+
Streaming tools start only for local examples. They do not yet support:
|
|
194
|
+
|
|
195
|
+
- long-running streams opened with GET
|
|
196
|
+
- saved sessions, replay, or subscriptions
|
|
197
|
+
- proxy servers or recovery after reconnecting
|
|
198
|
+
- slowing the producer when a client cannot keep up
|
|
199
|
+
- more than one server instance
|
|
200
|
+
|
|
201
|
+
## Public Resources and Prompts
|
|
202
|
+
|
|
203
|
+
Static resources and prompts are public operations. Their handlers receive only
|
|
204
|
+
the shared deadline and cancellation signal. The framework checks prompt
|
|
205
|
+
arguments and results before returning them.
|
|
206
|
+
|
|
207
|
+
`operationTimeoutMs` limits tool calls, resource reads, and prompt requests. It
|
|
208
|
+
defaults to 30 seconds.
|
|
209
|
+
|
|
210
|
+
`maxApplicationResultBytes` limits each checked handler result before MCP
|
|
211
|
+
encoding. It defaults to one mebibyte. The limit does not include MCP metadata,
|
|
212
|
+
protocol envelopes, discovery, or catalogues.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import {
|
|
216
|
+
createEmseepea,
|
|
217
|
+
definePrompt,
|
|
218
|
+
defineResource,
|
|
219
|
+
defineResourceTemplate,
|
|
220
|
+
} from "@emseepea/server";
|
|
221
|
+
import { z } from "zod";
|
|
222
|
+
|
|
223
|
+
const guideUri = "guide://coffee/getting-started";
|
|
224
|
+
|
|
225
|
+
const guide = defineResource({
|
|
226
|
+
name: "getting-started",
|
|
227
|
+
uri: guideUri,
|
|
228
|
+
mimeType: "text/markdown",
|
|
229
|
+
handler: () => ({ contents: [{ uri: guideUri, text: "# Brew safely" }] }),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const methodGuide = defineResourceTemplate({
|
|
233
|
+
name: "method-guide",
|
|
234
|
+
uriTemplate: "guide://coffee/method/{method}",
|
|
235
|
+
mimeType: "text/markdown",
|
|
236
|
+
complete: {
|
|
237
|
+
method: (value) => ["espresso", "pour-over"].filter((method) => method.startsWith(value)),
|
|
238
|
+
},
|
|
239
|
+
handler: ({ uri, variables }) => ({
|
|
240
|
+
contents: [{ uri, text: `# ${String(variables.method)}` }],
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const brew = definePrompt({
|
|
245
|
+
name: "brew-guide",
|
|
246
|
+
argsSchema: z.object({ topic: z.string().min(1) }),
|
|
247
|
+
complete: {
|
|
248
|
+
topic: (value) => ["grind-size", "water-temperature"].filter((topic) => topic.startsWith(value)),
|
|
249
|
+
},
|
|
250
|
+
handler: ({ topic }) => ({
|
|
251
|
+
messages: [{ role: "user", content: { type: "text", text: `Explain ${topic}.` } }],
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const app = createEmseepea({
|
|
256
|
+
name: "coffee",
|
|
257
|
+
version: "1.0.0",
|
|
258
|
+
resources: [guide, methodGuide],
|
|
259
|
+
prompts: [brew],
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Resource templates advertise an address pattern rather than listing every
|
|
264
|
+
possible address. Clients list the patterns, then read an address that matches
|
|
265
|
+
one. Each `{variable}` fills one complete path segment. The scheme and host
|
|
266
|
+
cannot change. Conflicting patterns stop the server at startup.
|
|
267
|
+
|
|
268
|
+
Suggestions are optional. When enabled, a handler receives the partial text,
|
|
269
|
+
the request time limit, cancellation, and registered string arguments. Em See
|
|
270
|
+
Pea checks the returned suggestions and keeps at most the first 100.
|
|
271
|
+
|
|
272
|
+
Suggestions are public even when some tools require sign-in. Return only text
|
|
273
|
+
that is safe for anyone to discover.
|
|
274
|
+
|
|
275
|
+
Listing every matching address, large result pages, sign-in for resources, and
|
|
276
|
+
custom caching are not included yet.
|
|
277
|
+
|
|
278
|
+
## Tool That Requires Sign-In
|
|
279
|
+
|
|
280
|
+
Tools that require sign-in declare their permissions. The framework leaves
|
|
281
|
+
`server/discover` and `tools/list` open to everyone. It verifies a bearer token
|
|
282
|
+
only when someone calls the restricted tool, then checks its expiry,
|
|
283
|
+
permissions, and intended server. The tool receives the caller's identity, not
|
|
284
|
+
the token itself.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const lookup = defineTool({
|
|
288
|
+
name: "lookup-private-bean",
|
|
289
|
+
access: "protected",
|
|
290
|
+
requiredScopes: ["beans:read"],
|
|
291
|
+
description: "Look up a private bean.",
|
|
292
|
+
inputSchema: z.object({ id: z.string() }),
|
|
293
|
+
outputSchema: z.object({ id: z.string(), clientId: z.string() }),
|
|
294
|
+
handler: ({ id }, { principal }) => ({
|
|
295
|
+
text: id,
|
|
296
|
+
data: { id, clientId: principal.clientId },
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const app = createEmseepea({
|
|
301
|
+
name: "beans",
|
|
302
|
+
version: "1.0.0",
|
|
303
|
+
tools: [lookup],
|
|
304
|
+
oauth: {
|
|
305
|
+
verifier,
|
|
306
|
+
metadata: {
|
|
307
|
+
resourceServerUrl: new URL("https://api.example/mcp"),
|
|
308
|
+
scopesSupported: ["beans:read"],
|
|
309
|
+
oauthMetadata,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The application's token checker must verify who issued the token and stop its
|
|
316
|
+
own slow network or file work. Em See Pea limits how long the request waits.
|
|
317
|
+
The official MCP library cannot pass a cancellation signal to the checker.
|
|
318
|
+
|
|
319
|
+
The application must decide whether the signed-in person may access each
|
|
320
|
+
record.
|
|
321
|
+
|
|
322
|
+
The HTTP client described above is only for public, read-only JSON APIs. The
|
|
323
|
+
application remains responsible for backend requests that use credentials.
|
package/package.json
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Fastify-first framework for Model Context Protocol 2026-07-28 servers over Streamable HTTP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/windyroad/emseepea.git",
|
|
9
9
|
"directory": "packages/framework"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/windyroad/emseepea#readme",
|
|
12
|
+
"bugs": "https://github.com/windyroad/emseepea/issues",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"provenance": true,
|
|
16
|
+
"tag": "next"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./ui": {
|
|
25
|
+
"types": "./dist/ui.d.ts",
|
|
26
|
+
"import": "./dist/ui.js"
|
|
27
|
+
},
|
|
28
|
+
"./http": {
|
|
29
|
+
"types": "./dist/http.d.ts",
|
|
30
|
+
"import": "./dist/http.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.json"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/core": "2.0.0",
|
|
43
|
+
"@modelcontextprotocol/fastify": "2.0.0",
|
|
44
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
45
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
46
|
+
"fastify": "5.12.1",
|
|
47
|
+
"zod": "4.4.3"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
10
51
|
}
|
|
11
52
|
}
|