sello 0.1.13 → 0.1.15
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 -1
- package/dist/cli/sello.js +65 -8
- package/dist/sdk/service.js +246 -85
- package/docs/sdk-quickstart.md +29 -0
- package/docs/sdk-security-audit.md +1 -0
- package/package.json +1 -1
- package/sdks/typescript/examples/mcp-minimal-server.ts +9 -17
- package/sdks/typescript/examples/mcp-tool-server.ts +13 -14
- package/sdks/typescript/src/cli/sello.ts +65 -8
- package/sdks/typescript/src/sdk/service.ts +246 -85
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="#try-it">Quickstart</a> ·
|
|
18
18
|
<a href="#add-sello-to-a-tool">Add Sello</a> ·
|
|
19
|
+
<a href="#add-sello-to-an-mcp-server">MCP</a> ·
|
|
19
20
|
<a href="sdks/README.md">SDKs</a> ·
|
|
20
21
|
<a href="#see-logged-actions">Actions</a> ·
|
|
21
22
|
<a href="#how-it-works">How It Works</a> ·
|
|
@@ -116,6 +117,39 @@ npx --yes sello init-demo
|
|
|
116
117
|
npx --yes sello init-http-demo
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
## Add Sello to an MCP Server
|
|
121
|
+
|
|
122
|
+
If your service exposes MCP tools, wrap the tool callback:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { sello } from "sello";
|
|
126
|
+
|
|
127
|
+
const receipts = sello.service();
|
|
128
|
+
|
|
129
|
+
server.registerTool(
|
|
130
|
+
"calendar.create_event",
|
|
131
|
+
{ inputSchema: createEventInputSchema },
|
|
132
|
+
receipts.mcpTool("calendar.create_event", async (args) => {
|
|
133
|
+
const event = await calendar.events.create(args);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: event.id }],
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Some MCP SDK versions call this method `tool` instead of `registerTool`; use the same callback slot either way.
|
|
142
|
+
|
|
143
|
+
`mcpTool` verifies the agent token before your callback runs, preserves the callback's return value, rethrows callback errors, and emits a receipt with action type `mcp.tools/call.<tool-name>`.
|
|
144
|
+
|
|
145
|
+
By default, Sello looks for an `Authorization: Bearer ...` token in common MCP context/header fields. If your transport stores tokens somewhere else, pass an extractor:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
receipts.mcpTool("calendar.create_event", handler, {
|
|
149
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
119
153
|
## See Logged Actions
|
|
120
154
|
|
|
121
155
|
```bash
|
|
@@ -155,7 +189,7 @@ Sello does not prove that the agent called every service it should have called,
|
|
|
155
189
|
- [Protocol Walkthrough](docs/protocol-walkthrough.md): the primitive receipt loop for implementers.
|
|
156
190
|
- [SPEC.md](SPEC.md): the Sello protocol draft.
|
|
157
191
|
- [Notarized Agents paper](https://arxiv.org/abs/2606.04193): design rationale, threat model, and prior art.
|
|
158
|
-
- [sdks/typescript/examples/mcp-minimal-server.ts](sdks/typescript/examples/mcp-minimal-server.ts): a small MCP
|
|
192
|
+
- [sdks/typescript/examples/mcp-minimal-server.ts](sdks/typescript/examples/mcp-minimal-server.ts): a small MCP integration.
|
|
159
193
|
- [docs/security-review.md](docs/security-review.md) and [docs/sdk-security-audit.md](docs/sdk-security-audit.md): current review notes.
|
|
160
194
|
|
|
161
195
|
The TypeScript SDK is published on [npm](https://www.npmjs.com/package/sello) and lives in [`sdks/typescript/`](sdks/typescript/). The Python SDK is published on [PyPI](https://pypi.org/project/sello/) and lives in [`sdks/python/`](sdks/python/). Both SDKs support the same service-side `sello.service()` flow; Python uses the `@receipts.tool(...)` decorator. Live Rekor proof verification and production identity operations are still future work.
|
package/dist/cli/sello.js
CHANGED
|
@@ -199,6 +199,29 @@ async function devCommand(args ) {
|
|
|
199
199
|
console.log(`Local dev log: ${logPath}`);
|
|
200
200
|
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
201
201
|
});
|
|
202
|
+
server.on("error", (error) => {
|
|
203
|
+
if (isPortInUse(error)) {
|
|
204
|
+
console.error(`sello: port ${port} is already in use.`);
|
|
205
|
+
console.error("");
|
|
206
|
+
console.error("Try:");
|
|
207
|
+
console.error(` sello dev --port ${port + 1}`);
|
|
208
|
+
console.error("");
|
|
209
|
+
console.error("Or stop the process using that port:");
|
|
210
|
+
console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isListenError(error)) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
console.error(`sello: could not start dev server on port ${port}.`);
|
|
218
|
+
console.error(message);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error;
|
|
224
|
+
});
|
|
202
225
|
}
|
|
203
226
|
|
|
204
227
|
async function emitDemoCommand(args ) {
|
|
@@ -257,14 +280,7 @@ async function callHttpDemoCommand(args ) {
|
|
|
257
280
|
);
|
|
258
281
|
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
259
282
|
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
260
|
-
const response = await
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: {
|
|
263
|
-
authorization: `Bearer ${state.agentToken}`,
|
|
264
|
-
"content-type": "application/json",
|
|
265
|
-
},
|
|
266
|
-
body: JSON.stringify({ title }),
|
|
267
|
-
});
|
|
283
|
+
const response = await fetchHttpDemo(url, state.agentToken, title);
|
|
268
284
|
const responseText = await response.text();
|
|
269
285
|
|
|
270
286
|
if (!response.ok) {
|
|
@@ -283,6 +299,31 @@ async function callHttpDemoCommand(args ) {
|
|
|
283
299
|
console.log(` ${actionViewerUrl(state)}`);
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
async function fetchHttpDemo(
|
|
303
|
+
url ,
|
|
304
|
+
agentToken ,
|
|
305
|
+
title ,
|
|
306
|
+
) {
|
|
307
|
+
try {
|
|
308
|
+
return await fetch(url, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
authorization: `Bearer ${agentToken}`,
|
|
312
|
+
"content-type": "application/json",
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({ title }),
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (isFetchFailed(error)) {
|
|
318
|
+
throw new TypeError(
|
|
319
|
+
`could not reach the HTTP demo route at ${url}.\n\nStart it in another terminal:\n node sello-http-route.mjs`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
286
327
|
function initDemoCommand(args ) {
|
|
287
328
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
288
329
|
const force = args.includes("--force");
|
|
@@ -787,6 +828,22 @@ function isRecord(value ) {
|
|
|
787
828
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
788
829
|
}
|
|
789
830
|
|
|
831
|
+
function isPortInUse(error ) {
|
|
832
|
+
return (
|
|
833
|
+
isRecord(error) &&
|
|
834
|
+
error.code === "EADDRINUSE" &&
|
|
835
|
+
error.syscall === "listen"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function isListenError(error ) {
|
|
840
|
+
return isRecord(error) && error.syscall === "listen";
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function isFetchFailed(error ) {
|
|
844
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
845
|
+
}
|
|
846
|
+
|
|
790
847
|
function printHelp() {
|
|
791
848
|
console.log(`Usage:
|
|
792
849
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
package/dist/sdk/service.js
CHANGED
|
@@ -41,6 +41,36 @@ import {
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
44
74
|
|
|
45
75
|
|
|
46
76
|
|
|
@@ -65,6 +95,11 @@ import {
|
|
|
65
95
|
|
|
66
96
|
|
|
67
97
|
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
68
103
|
|
|
69
104
|
|
|
70
105
|
|
|
@@ -114,90 +149,126 @@ export function createSelloService(input ) {
|
|
|
114
149
|
return publisher;
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
152
|
+
function wrapTool (
|
|
153
|
+
actionType ,
|
|
154
|
+
handler ,
|
|
155
|
+
options = {},
|
|
156
|
+
) {
|
|
157
|
+
if (typeof actionType !== "string" || actionType.length === 0) {
|
|
158
|
+
throw new TypeError("Sello action type must be a non-empty string");
|
|
159
|
+
}
|
|
126
160
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
return async (request ) => {
|
|
162
|
+
const resolved = await config();
|
|
163
|
+
const authorizationToken = resolveValue(
|
|
164
|
+
options.authorizationToken ?? defaultAuthorizationToken,
|
|
165
|
+
request,
|
|
166
|
+
);
|
|
167
|
+
const tokenIssuerPublicKey = await resolveTokenIssuerPublicKey(resolved.tokenIssuer);
|
|
168
|
+
const verifiedToken = verifySelloJwsToken({
|
|
169
|
+
authorizationToken,
|
|
170
|
+
issuerPublicKey: tokenIssuerPublicKey,
|
|
171
|
+
});
|
|
172
|
+
const selloLogs = selectSelloLogs(
|
|
173
|
+
verifiedToken.selloLogs,
|
|
174
|
+
resolved.fallbackSelloLogs,
|
|
175
|
+
resolved.log.logUrl,
|
|
176
|
+
);
|
|
177
|
+
const base = {
|
|
178
|
+
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
179
|
+
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
180
|
+
selloLogs,
|
|
181
|
+
serviceKid: resolved.serviceKid,
|
|
182
|
+
servicePrivateKey: resolved.servicePrivateKey,
|
|
183
|
+
serviceIdentifier: resolved.service,
|
|
184
|
+
logUrl: resolved.log.logUrl,
|
|
185
|
+
actionType,
|
|
186
|
+
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
187
|
+
timestamp: resolved.now(),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (options.isDenied && (await options.isDenied(request))) {
|
|
191
|
+
const response = options.deniedResponse
|
|
192
|
+
? await options.deniedResponse(request)
|
|
193
|
+
: undefined;
|
|
194
|
+
const receipt = emitReceipt({
|
|
195
|
+
...base,
|
|
196
|
+
actionOutputBytes: new Uint8Array(),
|
|
197
|
+
resultStatus: "denied",
|
|
137
198
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
resolved.fallbackSelloLogs,
|
|
141
|
-
resolved.log.logUrl,
|
|
142
|
-
);
|
|
143
|
-
const base = {
|
|
144
|
-
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
145
|
-
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
146
|
-
selloLogs,
|
|
147
|
-
serviceKid: resolved.serviceKid,
|
|
148
|
-
servicePrivateKey: resolved.servicePrivateKey,
|
|
149
|
-
serviceIdentifier: resolved.service,
|
|
150
|
-
logUrl: resolved.log.logUrl,
|
|
151
|
-
actionType,
|
|
152
|
-
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
153
|
-
timestamp: resolved.now(),
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
if (options.isDenied && (await options.isDenied(request))) {
|
|
157
|
-
const response = options.deniedResponse
|
|
158
|
-
? await options.deniedResponse(request)
|
|
159
|
-
: undefined;
|
|
160
|
-
const receipt = emitReceipt({
|
|
161
|
-
...base,
|
|
162
|
-
actionOutputBytes: new Uint8Array(),
|
|
163
|
-
resultStatus: "denied",
|
|
164
|
-
});
|
|
165
|
-
resolved.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
166
|
-
await submit(resolved, receipt, base.timestamp);
|
|
167
|
-
|
|
168
|
-
if (options.deniedResponse) {
|
|
169
|
-
return response ;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
throw new SelloDeniedError(receipt);
|
|
173
|
-
}
|
|
199
|
+
resolved.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
200
|
+
await submit(resolved, receipt, base.timestamp);
|
|
174
201
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
response = await handler(request);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
const receipt = emitReceipt({
|
|
180
|
-
...base,
|
|
181
|
-
actionOutputBytes: (options.canonicalizeError ?? canonicalErrorBytes)(
|
|
182
|
-
error,
|
|
183
|
-
),
|
|
184
|
-
resultStatus: "error",
|
|
185
|
-
});
|
|
186
|
-
resolved.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
187
|
-
await submit(resolved, receipt, base.timestamp);
|
|
188
|
-
throw error;
|
|
202
|
+
if (options.deniedResponse) {
|
|
203
|
+
return response ;
|
|
189
204
|
}
|
|
190
205
|
|
|
206
|
+
throw new SelloDeniedError(receipt);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let response ;
|
|
210
|
+
try {
|
|
211
|
+
response = await handler(request);
|
|
212
|
+
} catch (error) {
|
|
191
213
|
const receipt = emitReceipt({
|
|
192
214
|
...base,
|
|
193
|
-
actionOutputBytes: (options.
|
|
194
|
-
|
|
215
|
+
actionOutputBytes: (options.canonicalizeError ?? canonicalErrorBytes)(
|
|
216
|
+
error,
|
|
195
217
|
),
|
|
196
|
-
resultStatus: "
|
|
218
|
+
resultStatus: "error",
|
|
197
219
|
});
|
|
198
|
-
resolved.onReceipt?.({ resultStatus: "
|
|
220
|
+
resolved.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
199
221
|
await submit(resolved, receipt, base.timestamp);
|
|
200
|
-
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const receipt = emitReceipt({
|
|
226
|
+
...base,
|
|
227
|
+
actionOutputBytes: (options.canonicalizeOutput ?? canonicalJsonBytes)(
|
|
228
|
+
response,
|
|
229
|
+
),
|
|
230
|
+
resultStatus: "success",
|
|
231
|
+
});
|
|
232
|
+
resolved.onReceipt?.({ resultStatus: "success", receipt, response });
|
|
233
|
+
await submit(resolved, receipt, base.timestamp);
|
|
234
|
+
return response;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
tool (
|
|
240
|
+
actionType ,
|
|
241
|
+
handler ,
|
|
242
|
+
options = {},
|
|
243
|
+
) {
|
|
244
|
+
return wrapTool(actionType, handler, options);
|
|
245
|
+
},
|
|
246
|
+
mcpTool (
|
|
247
|
+
name ,
|
|
248
|
+
handler ,
|
|
249
|
+
options = {},
|
|
250
|
+
) {
|
|
251
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
252
|
+
throw new TypeError("MCP tool name must be a non-empty string");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const wrapped = wrapTool (
|
|
256
|
+
options.actionType ?? `mcp.tools/call.${name}`,
|
|
257
|
+
async (invocation) => handler(invocation.arguments, invocation.context),
|
|
258
|
+
{
|
|
259
|
+
authorizationToken:
|
|
260
|
+
options.authorizationToken ?? defaultMcpAuthorizationToken,
|
|
261
|
+
canonicalizeInput:
|
|
262
|
+
options.canonicalizeInput ?? defaultMcpCanonicalizeInput,
|
|
263
|
+
canonicalizeOutput: options.canonicalizeOutput,
|
|
264
|
+
canonicalizeError: options.canonicalizeError,
|
|
265
|
+
isDenied: options.isDenied,
|
|
266
|
+
deniedResponse: options.deniedResponse,
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return async (args , context ) => {
|
|
271
|
+
return await wrapped({ name, arguments: args, context });
|
|
201
272
|
};
|
|
202
273
|
},
|
|
203
274
|
async flush() {
|
|
@@ -481,23 +552,113 @@ function selectSelloLogs(
|
|
|
481
552
|
}
|
|
482
553
|
|
|
483
554
|
function defaultAuthorizationToken(request ) {
|
|
555
|
+
const token = authorizationTokenFromUnknown(request);
|
|
556
|
+
if (token) {
|
|
557
|
+
return token;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
throw new TypeError(
|
|
561
|
+
"Sello authorization token not found. Pass authorizationToken or include request.authorizationToken.",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function defaultMcpAuthorizationToken(
|
|
566
|
+
invocation ,
|
|
567
|
+
) {
|
|
568
|
+
const fromContext = authorizationTokenFromUnknown(invocation.context);
|
|
569
|
+
if (fromContext) {
|
|
570
|
+
return fromContext;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
throw new TypeError(
|
|
574
|
+
"Sello MCP authorization token not found. Pass authorizationToken or include an Authorization header in the MCP context.",
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function defaultMcpCanonicalizeInput(
|
|
579
|
+
invocation ,
|
|
580
|
+
) {
|
|
581
|
+
return canonicalJsonBytes({
|
|
582
|
+
method: "tools/call",
|
|
583
|
+
params: {
|
|
584
|
+
name: invocation.name,
|
|
585
|
+
arguments: invocation.arguments,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function authorizationTokenFromUnknown(value ) {
|
|
591
|
+
if (!isRecord(value)) {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const direct = value.authorizationToken ?? value.authorization;
|
|
596
|
+
if (typeof direct === "string" || direct instanceof Uint8Array) {
|
|
597
|
+
return stripBearer(direct);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const headers = value.headers;
|
|
601
|
+
const fromHeaders = authorizationTokenFromHeaders(headers);
|
|
602
|
+
if (fromHeaders) {
|
|
603
|
+
return fromHeaders;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const requestInfo = value.requestInfo;
|
|
607
|
+
if (isRecord(requestInfo)) {
|
|
608
|
+
const fromRequestInfo = authorizationTokenFromHeaders(requestInfo.headers);
|
|
609
|
+
if (fromRequestInfo) {
|
|
610
|
+
return fromRequestInfo;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const request = value.request;
|
|
484
615
|
if (isRecord(request)) {
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
return
|
|
616
|
+
const fromRequest = authorizationTokenFromHeaders(request.headers);
|
|
617
|
+
if (fromRequest) {
|
|
618
|
+
return fromRequest;
|
|
488
619
|
}
|
|
620
|
+
}
|
|
489
621
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
622
|
+
const authInfo = value.authInfo;
|
|
623
|
+
if (isRecord(authInfo)) {
|
|
624
|
+
const token = authInfo.token ?? authInfo.accessToken ?? authInfo.access_token;
|
|
625
|
+
if (typeof token === "string" || token instanceof Uint8Array) {
|
|
626
|
+
return stripBearer(token);
|
|
496
627
|
}
|
|
497
628
|
}
|
|
498
629
|
|
|
499
|
-
|
|
500
|
-
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function authorizationTokenFromHeaders(
|
|
634
|
+
headers ,
|
|
635
|
+
) {
|
|
636
|
+
if (isHeadersLike(headers)) {
|
|
637
|
+
const header = headers.get("authorization") ?? headers.get("Authorization");
|
|
638
|
+
if (typeof header === "string") {
|
|
639
|
+
return stripBearer(header);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!isRecord(headers)) {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const header = headers.authorization ?? headers.Authorization;
|
|
648
|
+
if (typeof header === "string" || header instanceof Uint8Array) {
|
|
649
|
+
return stripBearer(header);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function isHeadersLike(
|
|
656
|
+
value ,
|
|
657
|
+
) {
|
|
658
|
+
return (
|
|
659
|
+
typeof value === "object" &&
|
|
660
|
+
value !== null &&
|
|
661
|
+
typeof (value ).get === "function"
|
|
501
662
|
);
|
|
502
663
|
}
|
|
503
664
|
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -97,6 +97,35 @@ node --run example:tool
|
|
|
97
97
|
node --run actions
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
For an MCP tool, wrap the callback you already register:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { sello } from "sello";
|
|
104
|
+
|
|
105
|
+
const receipts = sello.service();
|
|
106
|
+
|
|
107
|
+
server.registerTool(
|
|
108
|
+
"calendar.create_event",
|
|
109
|
+
{ inputSchema: createEventInputSchema },
|
|
110
|
+
receipts.mcpTool("calendar.create_event", async (args) => {
|
|
111
|
+
const event = await calendar.events.create(args);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: event.id }],
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Some MCP SDK versions call this method `tool` instead of `registerTool`; use the same callback slot either way.
|
|
120
|
+
|
|
121
|
+
`receipts.mcpTool(...)` uses action type `mcp.tools/call.<tool-name>` and hashes only the MCP method name, tool name, and arguments. It tries common MCP context/header locations for the bearer token. If your transport puts the token somewhere else, pass an extractor:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
receipts.mcpTool("calendar.create_event", handler, {
|
|
125
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
100
129
|
Or run the matching Python example:
|
|
101
130
|
|
|
102
131
|
```bash
|
|
@@ -29,6 +29,7 @@ These notes cover the first Stripe-style SDK implementation pass. They are writt
|
|
|
29
29
|
- Invalid tokens prevent the handler from running and emit no receipt.
|
|
30
30
|
- Success, error, and denied paths emit receipts without including plaintext request or response bodies.
|
|
31
31
|
- The wrapper uses the configured service identity and key for every receipt.
|
|
32
|
+
- `receipts.mcpTool(...)` reuses the same wrapper path. It defaults to hashing the MCP `tools/call` method, tool name, and arguments, while excluding bearer tokens and transport context from the action input hash.
|
|
32
33
|
|
|
33
34
|
## Phase 4: Logs And Action Viewing
|
|
34
35
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canonicalJsonBytes,
|
|
3
|
-
sello,
|
|
4
|
-
type SelloReceipts,
|
|
5
|
-
} from "../src/index.ts";
|
|
1
|
+
import { sello, type SelloReceipts } from "../src/index.ts";
|
|
6
2
|
|
|
7
3
|
export type MinimalMcpRequest = {
|
|
8
4
|
headers: {
|
|
@@ -35,10 +31,13 @@ export type MinimalMcpResponse = {
|
|
|
35
31
|
export function createCalendarMcpServer(
|
|
36
32
|
receipts: SelloReceipts = sello.service(),
|
|
37
33
|
) {
|
|
38
|
-
const createEvent = receipts.
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const createEvent = receipts.mcpTool<
|
|
35
|
+
MinimalMcpRequest["body"]["params"]["arguments"],
|
|
36
|
+
MinimalMcpResponse,
|
|
37
|
+
MinimalMcpRequest
|
|
38
|
+
>(
|
|
39
|
+
"calendar.create_event",
|
|
40
|
+
async (args, request) => {
|
|
42
41
|
const title = readString(args.title, "title");
|
|
43
42
|
|
|
44
43
|
return {
|
|
@@ -54,13 +53,6 @@ export function createCalendarMcpServer(
|
|
|
54
53
|
},
|
|
55
54
|
};
|
|
56
55
|
},
|
|
57
|
-
{
|
|
58
|
-
canonicalizeInput: (request) => canonicalJsonBytes({
|
|
59
|
-
method: request.body.method,
|
|
60
|
-
params: request.body.params,
|
|
61
|
-
}),
|
|
62
|
-
canonicalizeOutput: (response) => canonicalJsonBytes(response),
|
|
63
|
-
},
|
|
64
56
|
);
|
|
65
57
|
|
|
66
58
|
return {
|
|
@@ -79,7 +71,7 @@ export function createCalendarMcpServer(
|
|
|
79
71
|
};
|
|
80
72
|
}
|
|
81
73
|
|
|
82
|
-
return await createEvent(request);
|
|
74
|
+
return await createEvent(request.body.params.arguments, request);
|
|
83
75
|
},
|
|
84
76
|
|
|
85
77
|
flush: () => receipts.flush(),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
canonicalJsonBytes,
|
|
7
6
|
sello,
|
|
8
7
|
type SdkSubmissionLog,
|
|
9
8
|
type SelloReceipts,
|
|
@@ -76,7 +75,10 @@ export type McpToolServerExampleOptions = {
|
|
|
76
75
|
export function createSelloMcpToolServer(receipts: SelloReceipts): SelloMcpToolServer {
|
|
77
76
|
const tools = new Map<
|
|
78
77
|
string,
|
|
79
|
-
(
|
|
78
|
+
(
|
|
79
|
+
args: Record<string, unknown>,
|
|
80
|
+
request: McpHttpToolCall,
|
|
81
|
+
) => Promise<McpHttpToolResponse>
|
|
80
82
|
>();
|
|
81
83
|
|
|
82
84
|
return {
|
|
@@ -88,23 +90,20 @@ export function createSelloMcpToolServer(receipts: SelloReceipts): SelloMcpToolS
|
|
|
88
90
|
throw new TypeError(`MCP tool ${name} is already registered`);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
const wrapped = receipts.
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
const wrapped = receipts.mcpTool<
|
|
94
|
+
Record<string, unknown>,
|
|
95
|
+
McpHttpToolResponse,
|
|
96
|
+
McpHttpToolCall
|
|
97
|
+
>(
|
|
98
|
+
name,
|
|
99
|
+
async (args, request) => ({
|
|
94
100
|
status: 200,
|
|
95
101
|
body: {
|
|
96
102
|
jsonrpc: "2.0",
|
|
97
103
|
id: request.body.id,
|
|
98
|
-
result: await handler(
|
|
104
|
+
result: await handler(args),
|
|
99
105
|
},
|
|
100
106
|
}),
|
|
101
|
-
{
|
|
102
|
-
canonicalizeInput: (request) => canonicalJsonBytes({
|
|
103
|
-
method: request.body.method,
|
|
104
|
-
params: request.body.params,
|
|
105
|
-
}),
|
|
106
|
-
canonicalizeOutput: (response) => canonicalJsonBytes(response.body),
|
|
107
|
-
},
|
|
108
107
|
);
|
|
109
108
|
|
|
110
109
|
tools.set(name, wrapped);
|
|
@@ -120,7 +119,7 @@ export function createSelloMcpToolServer(receipts: SelloReceipts): SelloMcpToolS
|
|
|
120
119
|
return jsonRpcError(request.body.id, -32601, "tool not found");
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
return await tool(request);
|
|
122
|
+
return await tool(request.body.params.arguments, request);
|
|
124
123
|
},
|
|
125
124
|
};
|
|
126
125
|
}
|
|
@@ -200,6 +200,29 @@ async function devCommand(args: string[]): Promise<void> {
|
|
|
200
200
|
console.log(`Local dev log: ${logPath}`);
|
|
201
201
|
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
202
202
|
});
|
|
203
|
+
server.on("error", (error) => {
|
|
204
|
+
if (isPortInUse(error)) {
|
|
205
|
+
console.error(`sello: port ${port} is already in use.`);
|
|
206
|
+
console.error("");
|
|
207
|
+
console.error("Try:");
|
|
208
|
+
console.error(` sello dev --port ${port + 1}`);
|
|
209
|
+
console.error("");
|
|
210
|
+
console.error("Or stop the process using that port:");
|
|
211
|
+
console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isListenError(error)) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
console.error(`sello: could not start dev server on port ${port}.`);
|
|
219
|
+
console.error(message);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw error;
|
|
225
|
+
});
|
|
203
226
|
}
|
|
204
227
|
|
|
205
228
|
async function emitDemoCommand(args: string[]): Promise<void> {
|
|
@@ -258,14 +281,7 @@ async function callHttpDemoCommand(args: string[]): Promise<void> {
|
|
|
258
281
|
);
|
|
259
282
|
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
260
283
|
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
261
|
-
const response = await
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: {
|
|
264
|
-
authorization: `Bearer ${state.agentToken}`,
|
|
265
|
-
"content-type": "application/json",
|
|
266
|
-
},
|
|
267
|
-
body: JSON.stringify({ title }),
|
|
268
|
-
});
|
|
284
|
+
const response = await fetchHttpDemo(url, state.agentToken, title);
|
|
269
285
|
const responseText = await response.text();
|
|
270
286
|
|
|
271
287
|
if (!response.ok) {
|
|
@@ -284,6 +300,31 @@ async function callHttpDemoCommand(args: string[]): Promise<void> {
|
|
|
284
300
|
console.log(` ${actionViewerUrl(state)}`);
|
|
285
301
|
}
|
|
286
302
|
|
|
303
|
+
async function fetchHttpDemo(
|
|
304
|
+
url: string,
|
|
305
|
+
agentToken: string,
|
|
306
|
+
title: string,
|
|
307
|
+
): Promise<Response> {
|
|
308
|
+
try {
|
|
309
|
+
return await fetch(url, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
authorization: `Bearer ${agentToken}`,
|
|
313
|
+
"content-type": "application/json",
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({ title }),
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isFetchFailed(error)) {
|
|
319
|
+
throw new TypeError(
|
|
320
|
+
`could not reach the HTTP demo route at ${url}.\n\nStart it in another terminal:\n node sello-http-route.mjs`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
287
328
|
function initDemoCommand(args: string[]): void {
|
|
288
329
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
289
330
|
const force = args.includes("--force");
|
|
@@ -788,6 +829,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
788
829
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
789
830
|
}
|
|
790
831
|
|
|
832
|
+
function isPortInUse(error: unknown): boolean {
|
|
833
|
+
return (
|
|
834
|
+
isRecord(error) &&
|
|
835
|
+
error.code === "EADDRINUSE" &&
|
|
836
|
+
error.syscall === "listen"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function isListenError(error: unknown): boolean {
|
|
841
|
+
return isRecord(error) && error.syscall === "listen";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function isFetchFailed(error: unknown): boolean {
|
|
845
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
846
|
+
}
|
|
847
|
+
|
|
791
848
|
function printHelp(): void {
|
|
792
849
|
console.log(`Usage:
|
|
793
850
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
@@ -42,6 +42,36 @@ export type SelloToolOptions<Request, Response> = {
|
|
|
42
42
|
deniedResponse?: (request: Request) => Response | Promise<Response>;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
export type SelloMcpToolInvocation<Args, Context = unknown> = {
|
|
46
|
+
name: string;
|
|
47
|
+
arguments: Args;
|
|
48
|
+
context: Context;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SelloMcpToolHandler<Args, Response, Context = unknown> = (
|
|
52
|
+
args: Args,
|
|
53
|
+
context: Context,
|
|
54
|
+
) => Response | Promise<Response>;
|
|
55
|
+
|
|
56
|
+
export type SelloMcpToolOptions<Args, Response, Context = unknown> = {
|
|
57
|
+
actionType?: string;
|
|
58
|
+
authorizationToken?: SelloValueOrGetter<
|
|
59
|
+
SelloMcpToolInvocation<Args, Context>,
|
|
60
|
+
string | Uint8Array
|
|
61
|
+
>;
|
|
62
|
+
canonicalizeInput?: (
|
|
63
|
+
invocation: SelloMcpToolInvocation<Args, Context>,
|
|
64
|
+
) => Uint8Array;
|
|
65
|
+
canonicalizeOutput?: (response: Response) => Uint8Array;
|
|
66
|
+
canonicalizeError?: (error: unknown) => Uint8Array;
|
|
67
|
+
isDenied?: (
|
|
68
|
+
invocation: SelloMcpToolInvocation<Args, Context>,
|
|
69
|
+
) => boolean | Promise<boolean>;
|
|
70
|
+
deniedResponse?: (
|
|
71
|
+
invocation: SelloMcpToolInvocation<Args, Context>,
|
|
72
|
+
) => Response | Promise<Response>;
|
|
73
|
+
};
|
|
74
|
+
|
|
45
75
|
export type SelloServiceConfig = {
|
|
46
76
|
service?: string;
|
|
47
77
|
serviceKey?: ServiceKeyInput;
|
|
@@ -66,6 +96,11 @@ export type SelloReceipts = {
|
|
|
66
96
|
handler: SelloToolHandler<Request, Response>,
|
|
67
97
|
options?: SelloToolOptions<Request, Response>,
|
|
68
98
|
): SelloToolHandler<Request, Response>;
|
|
99
|
+
mcpTool<Args, Response, Context = unknown>(
|
|
100
|
+
name: string,
|
|
101
|
+
handler: SelloMcpToolHandler<Args, Response, Context>,
|
|
102
|
+
options?: SelloMcpToolOptions<Args, Response, Context>,
|
|
103
|
+
): SelloMcpToolHandler<Args, Response, Context>;
|
|
69
104
|
flush(): Promise<void>;
|
|
70
105
|
};
|
|
71
106
|
|
|
@@ -115,90 +150,126 @@ export function createSelloService(input?: SelloServiceInput): SelloReceipts {
|
|
|
115
150
|
return publisher;
|
|
116
151
|
}
|
|
117
152
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
153
|
+
function wrapTool<Request, Response>(
|
|
154
|
+
actionType: string,
|
|
155
|
+
handler: SelloToolHandler<Request, Response>,
|
|
156
|
+
options: SelloToolOptions<Request, Response> = {},
|
|
157
|
+
): SelloToolHandler<Request, Response> {
|
|
158
|
+
if (typeof actionType !== "string" || actionType.length === 0) {
|
|
159
|
+
throw new TypeError("Sello action type must be a non-empty string");
|
|
160
|
+
}
|
|
127
161
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
162
|
+
return async (request: Request): Promise<Response> => {
|
|
163
|
+
const resolved = await config();
|
|
164
|
+
const authorizationToken = resolveValue(
|
|
165
|
+
options.authorizationToken ?? defaultAuthorizationToken,
|
|
166
|
+
request,
|
|
167
|
+
);
|
|
168
|
+
const tokenIssuerPublicKey = await resolveTokenIssuerPublicKey(resolved.tokenIssuer);
|
|
169
|
+
const verifiedToken = verifySelloJwsToken({
|
|
170
|
+
authorizationToken,
|
|
171
|
+
issuerPublicKey: tokenIssuerPublicKey,
|
|
172
|
+
});
|
|
173
|
+
const selloLogs = selectSelloLogs(
|
|
174
|
+
verifiedToken.selloLogs,
|
|
175
|
+
resolved.fallbackSelloLogs,
|
|
176
|
+
resolved.log.logUrl,
|
|
177
|
+
);
|
|
178
|
+
const base = {
|
|
179
|
+
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
180
|
+
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
181
|
+
selloLogs,
|
|
182
|
+
serviceKid: resolved.serviceKid,
|
|
183
|
+
servicePrivateKey: resolved.servicePrivateKey,
|
|
184
|
+
serviceIdentifier: resolved.service,
|
|
185
|
+
logUrl: resolved.log.logUrl,
|
|
186
|
+
actionType,
|
|
187
|
+
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
188
|
+
timestamp: resolved.now(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (options.isDenied && (await options.isDenied(request))) {
|
|
192
|
+
const response = options.deniedResponse
|
|
193
|
+
? await options.deniedResponse(request)
|
|
194
|
+
: undefined;
|
|
195
|
+
const receipt = emitReceipt({
|
|
196
|
+
...base,
|
|
197
|
+
actionOutputBytes: new Uint8Array(),
|
|
198
|
+
resultStatus: "denied",
|
|
138
199
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
resolved.fallbackSelloLogs,
|
|
142
|
-
resolved.log.logUrl,
|
|
143
|
-
);
|
|
144
|
-
const base = {
|
|
145
|
-
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
146
|
-
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
147
|
-
selloLogs,
|
|
148
|
-
serviceKid: resolved.serviceKid,
|
|
149
|
-
servicePrivateKey: resolved.servicePrivateKey,
|
|
150
|
-
serviceIdentifier: resolved.service,
|
|
151
|
-
logUrl: resolved.log.logUrl,
|
|
152
|
-
actionType,
|
|
153
|
-
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
154
|
-
timestamp: resolved.now(),
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
if (options.isDenied && (await options.isDenied(request))) {
|
|
158
|
-
const response = options.deniedResponse
|
|
159
|
-
? await options.deniedResponse(request)
|
|
160
|
-
: undefined;
|
|
161
|
-
const receipt = emitReceipt({
|
|
162
|
-
...base,
|
|
163
|
-
actionOutputBytes: new Uint8Array(),
|
|
164
|
-
resultStatus: "denied",
|
|
165
|
-
});
|
|
166
|
-
resolved.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
167
|
-
await submit(resolved, receipt, base.timestamp);
|
|
168
|
-
|
|
169
|
-
if (options.deniedResponse) {
|
|
170
|
-
return response as Response;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
throw new SelloDeniedError(receipt);
|
|
174
|
-
}
|
|
200
|
+
resolved.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
201
|
+
await submit(resolved, receipt, base.timestamp);
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
response = await handler(request);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
const receipt = emitReceipt({
|
|
181
|
-
...base,
|
|
182
|
-
actionOutputBytes: (options.canonicalizeError ?? canonicalErrorBytes)(
|
|
183
|
-
error,
|
|
184
|
-
),
|
|
185
|
-
resultStatus: "error",
|
|
186
|
-
});
|
|
187
|
-
resolved.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
188
|
-
await submit(resolved, receipt, base.timestamp);
|
|
189
|
-
throw error;
|
|
203
|
+
if (options.deniedResponse) {
|
|
204
|
+
return response as Response;
|
|
190
205
|
}
|
|
191
206
|
|
|
207
|
+
throw new SelloDeniedError(receipt);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let response: Response;
|
|
211
|
+
try {
|
|
212
|
+
response = await handler(request);
|
|
213
|
+
} catch (error) {
|
|
192
214
|
const receipt = emitReceipt({
|
|
193
215
|
...base,
|
|
194
|
-
actionOutputBytes: (options.
|
|
195
|
-
|
|
216
|
+
actionOutputBytes: (options.canonicalizeError ?? canonicalErrorBytes)(
|
|
217
|
+
error,
|
|
196
218
|
),
|
|
197
|
-
resultStatus: "
|
|
219
|
+
resultStatus: "error",
|
|
198
220
|
});
|
|
199
|
-
resolved.onReceipt?.({ resultStatus: "
|
|
221
|
+
resolved.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
200
222
|
await submit(resolved, receipt, base.timestamp);
|
|
201
|
-
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const receipt = emitReceipt({
|
|
227
|
+
...base,
|
|
228
|
+
actionOutputBytes: (options.canonicalizeOutput ?? canonicalJsonBytes)(
|
|
229
|
+
response,
|
|
230
|
+
),
|
|
231
|
+
resultStatus: "success",
|
|
232
|
+
});
|
|
233
|
+
resolved.onReceipt?.({ resultStatus: "success", receipt, response });
|
|
234
|
+
await submit(resolved, receipt, base.timestamp);
|
|
235
|
+
return response;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
tool<Request, Response>(
|
|
241
|
+
actionType: string,
|
|
242
|
+
handler: SelloToolHandler<Request, Response>,
|
|
243
|
+
options: SelloToolOptions<Request, Response> = {},
|
|
244
|
+
): SelloToolHandler<Request, Response> {
|
|
245
|
+
return wrapTool(actionType, handler, options);
|
|
246
|
+
},
|
|
247
|
+
mcpTool<Args, Response, Context = unknown>(
|
|
248
|
+
name: string,
|
|
249
|
+
handler: SelloMcpToolHandler<Args, Response, Context>,
|
|
250
|
+
options: SelloMcpToolOptions<Args, Response, Context> = {},
|
|
251
|
+
): SelloMcpToolHandler<Args, Response, Context> {
|
|
252
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
253
|
+
throw new TypeError("MCP tool name must be a non-empty string");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const wrapped = wrapTool<SelloMcpToolInvocation<Args, Context>, Response>(
|
|
257
|
+
options.actionType ?? `mcp.tools/call.${name}`,
|
|
258
|
+
async (invocation) => handler(invocation.arguments, invocation.context),
|
|
259
|
+
{
|
|
260
|
+
authorizationToken:
|
|
261
|
+
options.authorizationToken ?? defaultMcpAuthorizationToken,
|
|
262
|
+
canonicalizeInput:
|
|
263
|
+
options.canonicalizeInput ?? defaultMcpCanonicalizeInput,
|
|
264
|
+
canonicalizeOutput: options.canonicalizeOutput,
|
|
265
|
+
canonicalizeError: options.canonicalizeError,
|
|
266
|
+
isDenied: options.isDenied,
|
|
267
|
+
deniedResponse: options.deniedResponse,
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return async (args: Args, context: Context): Promise<Response> => {
|
|
272
|
+
return await wrapped({ name, arguments: args, context });
|
|
202
273
|
};
|
|
203
274
|
},
|
|
204
275
|
async flush(): Promise<void> {
|
|
@@ -482,23 +553,113 @@ function selectSelloLogs(
|
|
|
482
553
|
}
|
|
483
554
|
|
|
484
555
|
function defaultAuthorizationToken(request: unknown): string | Uint8Array {
|
|
556
|
+
const token = authorizationTokenFromUnknown(request);
|
|
557
|
+
if (token) {
|
|
558
|
+
return token;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
throw new TypeError(
|
|
562
|
+
"Sello authorization token not found. Pass authorizationToken or include request.authorizationToken.",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function defaultMcpAuthorizationToken(
|
|
567
|
+
invocation: SelloMcpToolInvocation<unknown, unknown>,
|
|
568
|
+
): string | Uint8Array {
|
|
569
|
+
const fromContext = authorizationTokenFromUnknown(invocation.context);
|
|
570
|
+
if (fromContext) {
|
|
571
|
+
return fromContext;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
throw new TypeError(
|
|
575
|
+
"Sello MCP authorization token not found. Pass authorizationToken or include an Authorization header in the MCP context.",
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function defaultMcpCanonicalizeInput(
|
|
580
|
+
invocation: SelloMcpToolInvocation<unknown, unknown>,
|
|
581
|
+
): Uint8Array {
|
|
582
|
+
return canonicalJsonBytes({
|
|
583
|
+
method: "tools/call",
|
|
584
|
+
params: {
|
|
585
|
+
name: invocation.name,
|
|
586
|
+
arguments: invocation.arguments,
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function authorizationTokenFromUnknown(value: unknown): string | Uint8Array | undefined {
|
|
592
|
+
if (!isRecord(value)) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const direct = value.authorizationToken ?? value.authorization;
|
|
597
|
+
if (typeof direct === "string" || direct instanceof Uint8Array) {
|
|
598
|
+
return stripBearer(direct);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const headers = value.headers;
|
|
602
|
+
const fromHeaders = authorizationTokenFromHeaders(headers);
|
|
603
|
+
if (fromHeaders) {
|
|
604
|
+
return fromHeaders;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const requestInfo = value.requestInfo;
|
|
608
|
+
if (isRecord(requestInfo)) {
|
|
609
|
+
const fromRequestInfo = authorizationTokenFromHeaders(requestInfo.headers);
|
|
610
|
+
if (fromRequestInfo) {
|
|
611
|
+
return fromRequestInfo;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const request = value.request;
|
|
485
616
|
if (isRecord(request)) {
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
617
|
+
const fromRequest = authorizationTokenFromHeaders(request.headers);
|
|
618
|
+
if (fromRequest) {
|
|
619
|
+
return fromRequest;
|
|
489
620
|
}
|
|
621
|
+
}
|
|
490
622
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
623
|
+
const authInfo = value.authInfo;
|
|
624
|
+
if (isRecord(authInfo)) {
|
|
625
|
+
const token = authInfo.token ?? authInfo.accessToken ?? authInfo.access_token;
|
|
626
|
+
if (typeof token === "string" || token instanceof Uint8Array) {
|
|
627
|
+
return stripBearer(token);
|
|
497
628
|
}
|
|
498
629
|
}
|
|
499
630
|
|
|
500
|
-
|
|
501
|
-
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function authorizationTokenFromHeaders(
|
|
635
|
+
headers: unknown,
|
|
636
|
+
): string | Uint8Array | undefined {
|
|
637
|
+
if (isHeadersLike(headers)) {
|
|
638
|
+
const header = headers.get("authorization") ?? headers.get("Authorization");
|
|
639
|
+
if (typeof header === "string") {
|
|
640
|
+
return stripBearer(header);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!isRecord(headers)) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const header = headers.authorization ?? headers.Authorization;
|
|
649
|
+
if (typeof header === "string" || header instanceof Uint8Array) {
|
|
650
|
+
return stripBearer(header);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isHeadersLike(
|
|
657
|
+
value: unknown,
|
|
658
|
+
): value is { get(name: string): string | null } {
|
|
659
|
+
return (
|
|
660
|
+
typeof value === "object" &&
|
|
661
|
+
value !== null &&
|
|
662
|
+
typeof (value as { get?: unknown }).get === "function"
|
|
502
663
|
);
|
|
503
664
|
}
|
|
504
665
|
|