sello 0.1.15 → 0.1.17
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 +39 -11
- package/dist/cli/sello.js +196 -4
- package/dist/sdk/service.js +129 -3
- package/docs/a2a.md +139 -0
- package/docs/mcp.md +149 -0
- package/docs/release-checklist.md +7 -0
- package/docs/sdk-quickstart.md +50 -1
- package/docs/sdk-security-audit.md +2 -0
- package/package.json +6 -2
- package/sdks/README.md +1 -1
- package/sdks/typescript/examples/a2a-minimal-server.ts +85 -0
- package/sdks/typescript/src/cli/sello.ts +196 -4
- package/sdks/typescript/src/sdk/service.ts +129 -3
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Add Sello to an MCP Server
|
|
2
|
+
|
|
3
|
+
Sello belongs around the tool callback: after your MCP server has identified the tool, before your business logic runs.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { sello } from "sello";
|
|
7
|
+
|
|
8
|
+
const receipts = sello.service();
|
|
9
|
+
|
|
10
|
+
server.registerTool(
|
|
11
|
+
"calendar.create_event",
|
|
12
|
+
{ inputSchema: createEventInputSchema },
|
|
13
|
+
receipts.mcpTool("calendar.create_event", async (args) => {
|
|
14
|
+
const event = await calendar.events.create(args);
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: event.id }],
|
|
17
|
+
};
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Some MCP SDK versions call this registration method `tool` instead of `registerTool`. Use the same callback slot either way.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
`receipts.mcpTool(...)` returns a normal MCP tool callback. When the agent calls the tool, Sello:
|
|
27
|
+
|
|
28
|
+
1. Reads the agent authorization token from the MCP context.
|
|
29
|
+
2. Verifies the token before your callback runs.
|
|
30
|
+
3. Runs your callback unchanged.
|
|
31
|
+
4. Emits a `success`, `error`, or `denied` receipt.
|
|
32
|
+
5. Preserves the callback return value and rethrows callback errors.
|
|
33
|
+
|
|
34
|
+
The default receipt action type is:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
mcp.tools/call.<tool-name>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For `calendar.create_event`, the action type is:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
mcp.tools/call.calendar.create_event
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Token Source
|
|
47
|
+
|
|
48
|
+
By default, Sello looks for `Authorization: Bearer ...` in common MCP context shapes, including:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
context.headers.authorization
|
|
52
|
+
context.requestInfo.headers.authorization
|
|
53
|
+
context.request.headers.authorization
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Fetch-style `Headers` objects are supported too.
|
|
57
|
+
|
|
58
|
+
If your transport stores the token somewhere else, pass an extractor:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
receipts.mcpTool("calendar.create_event", handler, {
|
|
62
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The token should be the same authorization token the agent used for the tool call. Sello hashes the exact token bytes into `sello_token_ref`; do not parse and reserialize it first.
|
|
67
|
+
|
|
68
|
+
## What Gets Hashed
|
|
69
|
+
|
|
70
|
+
The default MCP input hash covers only the stable MCP action shape:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"method": "tools/call",
|
|
75
|
+
"params": {
|
|
76
|
+
"name": "calendar.create_event",
|
|
77
|
+
"arguments": {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Sello does not hash:
|
|
83
|
+
|
|
84
|
+
- The bearer token.
|
|
85
|
+
- Transport headers.
|
|
86
|
+
- Connection/session objects.
|
|
87
|
+
- Other context that may vary across runtimes.
|
|
88
|
+
|
|
89
|
+
That keeps receipts stable without leaking secrets or transport details.
|
|
90
|
+
|
|
91
|
+
If you need a different hash boundary, pass `canonicalizeInput`.
|
|
92
|
+
|
|
93
|
+
## Unknown Tools
|
|
94
|
+
|
|
95
|
+
Only wrap tools you actually execute. If a request names an unknown tool, return your normal MCP `method not found` or `tool not found` error without emitting a Sello receipt.
|
|
96
|
+
|
|
97
|
+
The minimal example does this:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
if (request.body.params.name !== "calendar.create_event") {
|
|
101
|
+
return {
|
|
102
|
+
jsonrpc: "2.0",
|
|
103
|
+
id: request.body.id,
|
|
104
|
+
error: { code: -32601, message: "tool not found" },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## View Actions
|
|
110
|
+
|
|
111
|
+
In local development:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx sello dev
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Run your MCP tool call, then view verified actions:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx sello actions
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or open:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
http://localhost:8787/actions
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For a specific agent token:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx sello actions --token <agent-token>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Runnable Demo
|
|
136
|
+
|
|
137
|
+
To generate a tiny MCP-shaped demo in your project:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx --yes sello init-mcp-demo
|
|
141
|
+
node sello-mcp-demo.mjs
|
|
142
|
+
npx --yes sello actions
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Keep `npx sello dev` running in another terminal while you run the generated file.
|
|
146
|
+
|
|
147
|
+
## Example
|
|
148
|
+
|
|
149
|
+
See [`sdks/typescript/examples/mcp-minimal-server.ts`](../sdks/typescript/examples/mcp-minimal-server.ts) for a dependency-free MCP-shaped example. It wraps one `tools/call` handler and leaves unknown tools unreceipted.
|
|
@@ -16,6 +16,9 @@ Use this checklist before publishing a Sello release.
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
node --run test
|
|
19
|
+
node --run package:test
|
|
20
|
+
node --run package:a2a-test
|
|
21
|
+
node --run package:mcp-test
|
|
19
22
|
npm pack --dry-run
|
|
20
23
|
```
|
|
21
24
|
|
|
@@ -28,6 +31,8 @@ cd "$tmpdir/sello"
|
|
|
28
31
|
node -v # must be v22.7.0 or newer
|
|
29
32
|
node --run test
|
|
30
33
|
node --run package:test
|
|
34
|
+
node --run package:a2a-test
|
|
35
|
+
node --run package:mcp-test
|
|
31
36
|
npm pack --dry-run
|
|
32
37
|
node --experimental-strip-types sdks/typescript/src/cli/sello.ts --help
|
|
33
38
|
node --experimental-strip-types sdks/typescript/src/cli/sello.ts dev --dry-run
|
|
@@ -67,6 +72,8 @@ git push origin main --tags
|
|
|
67
72
|
After publishing:
|
|
68
73
|
|
|
69
74
|
- Confirm `npm view sello version` shows the new version.
|
|
75
|
+
- Confirm `SELLO_PACKAGE_SPEC=sello@$(node -p "require('./package.json').version") node --run package:a2a-test` passes against the published npm package.
|
|
76
|
+
- Confirm `SELLO_PACKAGE_SPEC=sello@$(node -p "require('./package.json').version") node --run package:mcp-test` passes against the published npm package.
|
|
70
77
|
- Publish a GitHub Release for the pushed tag so PyPI trusted publishing runs.
|
|
71
78
|
- Confirm `python -m pip index versions sello` shows the new version after PyPI publishing.
|
|
72
79
|
- Confirm `npx sello --help` works from a clean temp directory.
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -72,18 +72,43 @@ npx --yes sello call-http-demo
|
|
|
72
72
|
npx --yes sello actions
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
To try the same local loop around MCP or A2A-shaped handlers:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx --yes sello init-mcp-demo
|
|
79
|
+
node sello-mcp-demo.mjs
|
|
80
|
+
npx --yes sello actions
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx --yes sello init-a2a-demo
|
|
85
|
+
node sello-a2a-demo.mjs
|
|
86
|
+
npx --yes sello actions
|
|
87
|
+
```
|
|
88
|
+
|
|
75
89
|
## What Just Happened?
|
|
76
90
|
|
|
77
|
-
`sello dev` created local development keys, a demo authorization token, a service registry, and a local transparency log. The wrapped tool or
|
|
91
|
+
`sello dev` created local development keys, a demo authorization token, a service registry, and a local transparency log. The wrapped function, route, MCP tool, or A2A message callback verified the token before running your code. After your code returned, Sello signed an encrypted receipt for the observed action and submitted it to the local log. The log stored encrypted receipt data, not plaintext action details. `sello actions` used the owner key from local dev state to fetch, verify, decrypt, and print the action.
|
|
78
92
|
|
|
79
93
|
Local dev state lives under `.sello/`. The encrypted dev log is stored in `.sello/dev-log.jsonl`, so receipts survive restarting `sello dev` without being committed to git.
|
|
80
94
|
|
|
95
|
+
## Which Wrapper?
|
|
96
|
+
|
|
97
|
+
| Existing boundary | Use |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| Plain function or HTTP route | `receipts.tool("calendar.create_event", handler)` |
|
|
100
|
+
| MCP `tools/call` callback | `receipts.mcpTool("calendar.create_event", handler)` |
|
|
101
|
+
| A2A JSON-RPC message handler | `receipts.a2aMessage(handler)` |
|
|
102
|
+
| Owner action viewing | `npx sello actions` |
|
|
103
|
+
|
|
81
104
|
## Troubleshooting
|
|
82
105
|
|
|
83
106
|
- **Port already in use:** run `npx sello dev --port 8791`.
|
|
84
107
|
- **No actions found:** make sure `sello dev` is running from the same project folder where you emitted the receipt.
|
|
85
108
|
- **Missing token:** run `npx sello dev` first so `.sello/dev.json` exists.
|
|
86
109
|
|
|
110
|
+
## Contributing From This Repo
|
|
111
|
+
|
|
87
112
|
Inside this repo, start the local log and action viewer:
|
|
88
113
|
|
|
89
114
|
```bash
|
|
@@ -126,6 +151,30 @@ receipts.mcpTool("calendar.create_event", handler, {
|
|
|
126
151
|
});
|
|
127
152
|
```
|
|
128
153
|
|
|
154
|
+
For the complete MCP placement notes, see [MCP Integration](mcp.md).
|
|
155
|
+
|
|
156
|
+
For an A2A agent, wrap the message handler you already expose:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { sello } from "sello";
|
|
160
|
+
|
|
161
|
+
const receipts = sello.service();
|
|
162
|
+
|
|
163
|
+
export const sendMessage = receipts.a2aMessage(async (request, context) => {
|
|
164
|
+
return agent.handleMessage(request, context);
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`receipts.a2aMessage(...)` uses action type `a2a.<method>`, for example `a2a.message/send`, and hashes only the A2A JSON-RPC method and params. It excludes request ids, headers, bearer tokens, and runtime context. If your transport stores the token somewhere else, pass an extractor:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
receipts.a2aMessage(handler, {
|
|
172
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For the complete A2A placement notes, see [A2A Integration](a2a.md).
|
|
177
|
+
|
|
129
178
|
Or run the matching Python example:
|
|
130
179
|
|
|
131
180
|
```bash
|
|
@@ -30,6 +30,7 @@ These notes cover the first Stripe-style SDK implementation pass. They are writt
|
|
|
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
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.
|
|
33
|
+
- `receipts.a2aMessage(...)` reuses the same wrapper path. It defaults to hashing the A2A JSON-RPC method and params, while excluding bearer tokens, request ids, headers, and runtime context from the action input hash.
|
|
33
34
|
|
|
34
35
|
## Phase 4: Logs And Action Viewing
|
|
35
36
|
|
|
@@ -44,6 +45,7 @@ These notes cover the first Stripe-style SDK implementation pass. They are writt
|
|
|
44
45
|
- The example uses `submit: { mode: "await" }` so the command succeeds only after the receipt append completes.
|
|
45
46
|
- The example canonicalizes only tool input fields and excludes the authorization token wrapper from the action input hash.
|
|
46
47
|
- The MCP-style example reads the bearer token from the transport header, but hashes only the `tools/call` method and params.
|
|
48
|
+
- The A2A-style example reads the bearer token from the transport header, but hashes only the JSON-RPC method and params.
|
|
47
49
|
- README and quickstart docs keep self-hosting first-class and describe `sello.build` as optional convenience.
|
|
48
50
|
|
|
49
51
|
## Residual Risks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sello",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Reference implementation of the Sello protocol for service-signed AI agent receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
"bench": "node --experimental-strip-types sdks/typescript/src/cli/bench.ts",
|
|
32
32
|
"demo": "node --experimental-strip-types sdks/typescript/src/cli/demo.ts",
|
|
33
33
|
"dev": "node --experimental-strip-types sdks/typescript/src/cli/sello.ts dev",
|
|
34
|
+
"example:a2a:minimal": "node --test --experimental-strip-types sdks/typescript/test/examples/a2a-minimal-server.test.ts",
|
|
34
35
|
"example:mcp:minimal": "node --test --experimental-strip-types sdks/typescript/test/examples/mcp-minimal-server.test.ts",
|
|
35
36
|
"example:mcp": "node --experimental-strip-types sdks/typescript/examples/mcp-tool-server.ts",
|
|
36
37
|
"example:tool": "node --experimental-strip-types sdks/typescript/examples/quickstart-tool.ts",
|
|
37
38
|
"build": "node --disable-warning=ExperimentalWarning sdks/typescript/scripts/build-dist.mjs",
|
|
39
|
+
"package:a2a-test": "node sdks/typescript/scripts/package-a2a-smoke.mjs",
|
|
40
|
+
"package:mcp-test": "node sdks/typescript/scripts/package-mcp-smoke.mjs",
|
|
38
41
|
"package:test": "node sdks/typescript/scripts/package-smoke.mjs",
|
|
39
42
|
"pack:check": "node sdks/typescript/scripts/pack-check.mjs",
|
|
40
43
|
"prepack": "node --disable-warning=ExperimentalWarning sdks/typescript/scripts/build-dist.mjs",
|
|
@@ -49,7 +52,8 @@
|
|
|
49
52
|
"transparency-log",
|
|
50
53
|
"cose",
|
|
51
54
|
"hpke",
|
|
52
|
-
"mcp"
|
|
55
|
+
"mcp",
|
|
56
|
+
"a2a"
|
|
53
57
|
],
|
|
54
58
|
"publishConfig": {
|
|
55
59
|
"access": "public"
|
package/sdks/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The protocol, paper, and project docs live at the repository root. Language SDKs live here:
|
|
4
4
|
|
|
5
|
-
- [`typescript/`](typescript/): npm package source, CLI, examples, fixtures, and Node tests.
|
|
5
|
+
- [`typescript/`](typescript/): npm package source, CLI, MCP/A2A helpers, examples, fixtures, and Node tests.
|
|
6
6
|
- [`python/`](python/): Python package source and tests.
|
|
7
7
|
|
|
8
8
|
Root commands still work from the repository top level:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sello, type SelloReceipts } from "../src/index.ts";
|
|
2
|
+
|
|
3
|
+
export type MinimalA2aRequest = {
|
|
4
|
+
headers: {
|
|
5
|
+
authorization?: string;
|
|
6
|
+
Authorization?: string;
|
|
7
|
+
};
|
|
8
|
+
body: {
|
|
9
|
+
jsonrpc: "2.0";
|
|
10
|
+
id: string | number | null;
|
|
11
|
+
method: string;
|
|
12
|
+
params: {
|
|
13
|
+
message: {
|
|
14
|
+
role: "user" | "agent";
|
|
15
|
+
parts: { kind: "text"; text: string }[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MinimalA2aResponse = {
|
|
22
|
+
jsonrpc: "2.0";
|
|
23
|
+
id: string | number | null;
|
|
24
|
+
result?: {
|
|
25
|
+
kind: "message";
|
|
26
|
+
messageId: string;
|
|
27
|
+
role: "agent";
|
|
28
|
+
parts: { kind: "text"; text: string }[];
|
|
29
|
+
};
|
|
30
|
+
error?: {
|
|
31
|
+
code: number;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function createCalendarA2aAgent(
|
|
37
|
+
receipts: SelloReceipts = sello.service(),
|
|
38
|
+
) {
|
|
39
|
+
const sendMessage = receipts.a2aMessage<
|
|
40
|
+
MinimalA2aRequest["body"],
|
|
41
|
+
MinimalA2aResponse,
|
|
42
|
+
MinimalA2aRequest
|
|
43
|
+
>(async (body) => {
|
|
44
|
+
const text = body.params.message.parts
|
|
45
|
+
.filter((part) => part.kind === "text")
|
|
46
|
+
.map((part) => part.text)
|
|
47
|
+
.join(" ");
|
|
48
|
+
const title = text || "untitled";
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
id: body.id,
|
|
53
|
+
result: {
|
|
54
|
+
kind: "message",
|
|
55
|
+
messageId: "calendar-reply-1",
|
|
56
|
+
role: "agent",
|
|
57
|
+
parts: [
|
|
58
|
+
{
|
|
59
|
+
kind: "text",
|
|
60
|
+
text: `created ${title}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
async handle(request: MinimalA2aRequest): Promise<MinimalA2aResponse> {
|
|
69
|
+
if (request.body.method !== "message/send") {
|
|
70
|
+
return {
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
id: request.body.id,
|
|
73
|
+
error: {
|
|
74
|
+
code: -32601,
|
|
75
|
+
message: "method not found",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return await sendMessage(request.body, request);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
flush: () => receipts.flush(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -97,6 +97,12 @@ try {
|
|
|
97
97
|
case "init-http-demo":
|
|
98
98
|
initHttpDemoCommand(process.argv.slice(3));
|
|
99
99
|
break;
|
|
100
|
+
case "init-mcp-demo":
|
|
101
|
+
initMcpDemoCommand(process.argv.slice(3));
|
|
102
|
+
break;
|
|
103
|
+
case "init-a2a-demo":
|
|
104
|
+
initA2aDemoCommand(process.argv.slice(3));
|
|
105
|
+
break;
|
|
100
106
|
case "keys":
|
|
101
107
|
keysCommand(process.argv.slice(3));
|
|
102
108
|
break;
|
|
@@ -338,13 +344,13 @@ function initDemoCommand(args: string[]): void {
|
|
|
338
344
|
console.log(`Created ${output}`);
|
|
339
345
|
console.log("");
|
|
340
346
|
console.log("Terminal 1: keep the local dev log running");
|
|
341
|
-
console.log(
|
|
347
|
+
console.log(` ${devCommandHint()}`);
|
|
342
348
|
console.log("");
|
|
343
349
|
console.log("Terminal 2: emit and view a receipt");
|
|
344
350
|
console.log(` node ${output}`);
|
|
345
351
|
console.log(" npx sello actions");
|
|
346
352
|
console.log("");
|
|
347
|
-
console.log(
|
|
353
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
function initHttpDemoCommand(args: string[]): void {
|
|
@@ -360,7 +366,7 @@ function initHttpDemoCommand(args: string[]): void {
|
|
|
360
366
|
console.log(`Created ${output}`);
|
|
361
367
|
console.log("");
|
|
362
368
|
console.log("Terminal 1: keep the local dev log running");
|
|
363
|
-
console.log(
|
|
369
|
+
console.log(` ${devCommandHint()}`);
|
|
364
370
|
console.log("");
|
|
365
371
|
console.log("Terminal 2: start the route");
|
|
366
372
|
console.log(` node ${output}`);
|
|
@@ -369,7 +375,51 @@ function initHttpDemoCommand(args: string[]): void {
|
|
|
369
375
|
console.log(" npx sello call-http-demo");
|
|
370
376
|
console.log(" npx sello actions");
|
|
371
377
|
console.log("");
|
|
372
|
-
console.log(
|
|
378
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function initMcpDemoCommand(args: string[]): void {
|
|
382
|
+
const output = readFlag(args, "--output") ?? "sello-mcp-demo.mjs";
|
|
383
|
+
const force = args.includes("--force");
|
|
384
|
+
|
|
385
|
+
if (existsSync(output) && !force) {
|
|
386
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
writeFileSync(output, mcpDemoSource(), { mode: 0o644 });
|
|
390
|
+
|
|
391
|
+
console.log(`Created ${output}`);
|
|
392
|
+
console.log("");
|
|
393
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
394
|
+
console.log(` ${devCommandHint()}`);
|
|
395
|
+
console.log("");
|
|
396
|
+
console.log("Terminal 2: run the MCP-shaped tool call and view the receipt");
|
|
397
|
+
console.log(` node ${output}`);
|
|
398
|
+
console.log(" npx sello actions");
|
|
399
|
+
console.log("");
|
|
400
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function initA2aDemoCommand(args: string[]): void {
|
|
404
|
+
const output = readFlag(args, "--output") ?? "sello-a2a-demo.mjs";
|
|
405
|
+
const force = args.includes("--force");
|
|
406
|
+
|
|
407
|
+
if (existsSync(output) && !force) {
|
|
408
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
writeFileSync(output, a2aDemoSource(), { mode: 0o644 });
|
|
412
|
+
|
|
413
|
+
console.log(`Created ${output}`);
|
|
414
|
+
console.log("");
|
|
415
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
416
|
+
console.log(` ${devCommandHint()}`);
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log("Terminal 2: run the A2A-shaped message and view the receipt");
|
|
419
|
+
console.log(` node ${output}`);
|
|
420
|
+
console.log(" npx sello actions");
|
|
421
|
+
console.log("");
|
|
422
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
373
423
|
}
|
|
374
424
|
|
|
375
425
|
function keysCommand(args: string[]): void {
|
|
@@ -852,6 +902,8 @@ function printHelp(): void {
|
|
|
852
902
|
sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
|
|
853
903
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
854
904
|
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
905
|
+
sello init-mcp-demo [--output sello-mcp-demo.mjs] [--force]
|
|
906
|
+
sello init-a2a-demo [--output sello-a2a-demo.mjs] [--force]
|
|
855
907
|
sello actions [--token agent-token]
|
|
856
908
|
sello keys service
|
|
857
909
|
sello inspect-env
|
|
@@ -918,6 +970,25 @@ function formatHttpDemoResponse(responseText: string): string {
|
|
|
918
970
|
}
|
|
919
971
|
}
|
|
920
972
|
|
|
973
|
+
function devCommandHint(): string {
|
|
974
|
+
const state = loadDevStateIfPresent();
|
|
975
|
+
const port = state ? portFromDevState(state) : undefined;
|
|
976
|
+
return port && port !== "8787" ? `npx sello dev --port ${port}` : "npx sello dev";
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function actionViewerUrlHint(): string {
|
|
980
|
+
const state = loadDevStateIfPresent();
|
|
981
|
+
return state ? actionViewerUrl(state) : "http://localhost:8787/actions";
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function portFromDevState(state: DevState): string | undefined {
|
|
985
|
+
try {
|
|
986
|
+
return new URL(state.logEndpoint).port;
|
|
987
|
+
} catch {
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
921
992
|
function slug(value: string): string {
|
|
922
993
|
return value
|
|
923
994
|
.toLowerCase()
|
|
@@ -1081,3 +1152,124 @@ function slug(value) {
|
|
|
1081
1152
|
}
|
|
1082
1153
|
`;
|
|
1083
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
function mcpDemoSource(): string {
|
|
1157
|
+
return `import { readFileSync } from "node:fs";
|
|
1158
|
+
import { sello } from "sello";
|
|
1159
|
+
|
|
1160
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
1161
|
+
|
|
1162
|
+
const receipts = sello.service({
|
|
1163
|
+
service: state.serviceId,
|
|
1164
|
+
serviceKey: state.serviceKey,
|
|
1165
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
1166
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
1167
|
+
submit: { mode: "await" },
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const createEvent = receipts.mcpTool("calendar.create_event", async (args) => {
|
|
1171
|
+
const title = readString(args.title, "title");
|
|
1172
|
+
return {
|
|
1173
|
+
content: [
|
|
1174
|
+
{
|
|
1175
|
+
type: "text",
|
|
1176
|
+
text: "created " + title,
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
};
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
const response = await createEvent(
|
|
1183
|
+
{
|
|
1184
|
+
title: "MCP launch checklist",
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
requestInfo: {
|
|
1188
|
+
headers: new Headers({
|
|
1189
|
+
authorization: "Bearer " + state.agentToken,
|
|
1190
|
+
}),
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
await receipts.flush();
|
|
1196
|
+
|
|
1197
|
+
console.log("MCP tool response:");
|
|
1198
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1199
|
+
|
|
1200
|
+
function readString(value, name) {
|
|
1201
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1202
|
+
throw new TypeError(name + " must be a non-empty string");
|
|
1203
|
+
}
|
|
1204
|
+
return value;
|
|
1205
|
+
}
|
|
1206
|
+
`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function a2aDemoSource(): string {
|
|
1210
|
+
return `import { readFileSync } from "node:fs";
|
|
1211
|
+
import { sello } from "sello";
|
|
1212
|
+
|
|
1213
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
1214
|
+
|
|
1215
|
+
const receipts = sello.service({
|
|
1216
|
+
service: state.serviceId,
|
|
1217
|
+
serviceKey: state.serviceKey,
|
|
1218
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
1219
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
1220
|
+
submit: { mode: "await" },
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const sendMessage = receipts.a2aMessage(async (request) => ({
|
|
1224
|
+
jsonrpc: "2.0",
|
|
1225
|
+
id: request.id,
|
|
1226
|
+
result: {
|
|
1227
|
+
kind: "message",
|
|
1228
|
+
messageId: "calendar-reply-1",
|
|
1229
|
+
role: "agent",
|
|
1230
|
+
parts: [
|
|
1231
|
+
{
|
|
1232
|
+
kind: "text",
|
|
1233
|
+
text: "created " + readMessageText(request),
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
},
|
|
1237
|
+
}));
|
|
1238
|
+
|
|
1239
|
+
const response = await sendMessage(
|
|
1240
|
+
{
|
|
1241
|
+
jsonrpc: "2.0",
|
|
1242
|
+
id: "a2a-demo-1",
|
|
1243
|
+
method: "message/send",
|
|
1244
|
+
params: {
|
|
1245
|
+
message: {
|
|
1246
|
+
role: "user",
|
|
1247
|
+
parts: [
|
|
1248
|
+
{
|
|
1249
|
+
kind: "text",
|
|
1250
|
+
text: "A2A launch checklist",
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
headers: new Headers({
|
|
1258
|
+
authorization: "Bearer " + state.agentToken,
|
|
1259
|
+
}),
|
|
1260
|
+
},
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
await receipts.flush();
|
|
1264
|
+
|
|
1265
|
+
console.log("A2A message response:");
|
|
1266
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1267
|
+
|
|
1268
|
+
function readMessageText(request) {
|
|
1269
|
+
return request.params.message.parts
|
|
1270
|
+
.filter((part) => part.kind === "text")
|
|
1271
|
+
.map((part) => part.text)
|
|
1272
|
+
.join(" ") || "untitled";
|
|
1273
|
+
}
|
|
1274
|
+
`;
|
|
1275
|
+
}
|