@bnbagent/studio-cli 0.0.6-alpha.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/DISCLAIMER.md +48 -0
- package/LICENSE +201 -0
- package/dist/_agentcoreName-DZDWEYD3.js +7 -0
- package/dist/_twak-5XQMOFUC.js +25 -0
- package/dist/bag.js +19358 -0
- package/dist/chunk-7RAKL4AS.js +172 -0
- package/dist/chunk-M3ODFCA7.js +1053 -0
- package/dist/chunk-U7IDQ3K5.js +14 -0
- package/dist/deployCli-N6TPN6XA.js +40 -0
- package/package.json +64 -0
- package/recipes/agent/code/{{PKG}}/signing.ts.tmpl +287 -0
- package/recipes/agent/recipe.toml +35 -0
- package/recipes/providers/pieverse-llm/recipe.toml +16 -0
- package/recipes/providers/pieverse-llm/skills/funding-pieverse-llm.md +203 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/.dockerignore.tmpl +8 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +50 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/agentCard.ts.tmpl +135 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/dualMain.ts.tmpl +402 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +147 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +344 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/mcpMain.ts.tmpl +677 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/model.ts.tmpl +117 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/sellerCore.ts.tmpl +503 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/tools.ts.tmpl +157 -0
- package/recipes/runtimes/agentcore/recipe.toml +97 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/.dockerignore.tmpl +8 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +47 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +131 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +504 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +300 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/main.ts.tmpl +196 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +562 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/model.ts.tmpl +117 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/tools.ts.tmpl +157 -0
- package/recipes/runtimes/azure-foundry/recipe.toml +88 -0
- package/recipes/tools-chain/code/{{PKG}}/chainTools.ts.tmpl +166 -0
- package/recipes/tools-chain/recipe.toml +11 -0
- package/recipes/wallet/recipe.toml +20 -0
- package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +175 -0
- package/recipes/x402-buyer/recipe.toml +15 -0
- package/skills/bnbagent-studio.md +107 -0
- package/skills/references/bnbagent-studio-adding-to-project.md +241 -0
- package/skills/references/bnbagent-studio-buying-from-bazaar.md +169 -0
- package/skills/references/bnbagent-studio-buying-via-8183.md +222 -0
- package/skills/references/bnbagent-studio-extending-signing.md +227 -0
- package/skills/references/bnbagent-studio-operating.md +211 -0
- package/skills/references/bnbagent-studio-scaffolding-agent.md +536 -0
- package/skills/references/bnbagent-studio-selling-via-8183.md +271 -0
- package/skills/references/bnbagent-studio-selling-via-b402.md +194 -0
- package/skills/references/bnbagent-studio-use-aws-agentcore.md +208 -0
- package/skills/references/bnbagent-studio-use-azure-foundry.md +164 -0
- package/skills/references/bnbagent-studio-use-bnb-trial.md +92 -0
- package/skills/references/bnbagent-studio-using-altana-wallet.md +68 -0
- package/skills/references/bnbagent-studio-using-twak-wallet.md +260 -0
- package/skills/references/bnbagent-studio-wiring-llm-tools.md +338 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single MCP seller agent entrypoint (the `--protocol MCP` peer to main.ts).
|
|
3
|
+
*
|
|
4
|
+
* The x402 seller is intentionally dormant on Azure Foundry in v1. Use the
|
|
5
|
+
* platform deployment target for the anonymous HTTP-envelope tunnel.
|
|
6
|
+
*
|
|
7
|
+
* This is the VALUABLE agent AND the SOLE key-holder/signer, serving its
|
|
8
|
+
* seller surface over the **Model Context Protocol** instead of A2A. The MCP
|
|
9
|
+
* server is cloud-neutral: a streamable-HTTP MCP server on `0.0.0.0:8000/mcp`
|
|
10
|
+
* (port 8000, `AGENT_PORT` overrides locally) with no cloud-vendor serving
|
|
11
|
+
* dependency. `bag dev` runs `node mcpMain.js` directly. There is no separate
|
|
12
|
+
* forwarding service — the agent IS the seller.
|
|
13
|
+
*
|
|
14
|
+
* (For the azure-foundry runtime, the DEPLOY host speaks the Invocations
|
|
15
|
+
* protocol — `foundryMain.ts`. This MCP entrypoint is for LOCAL run / dev and
|
|
16
|
+
* the eventual cross-cloud transport.)
|
|
17
|
+
*
|
|
18
|
+
* MCP tools (all backed by signing.ts fixed code — NEVER LLM-callable):
|
|
19
|
+
*
|
|
20
|
+
* negotiate → read the FIXED list price → CLAMP to [min,max] → EIP-191 SIGN
|
|
21
|
+
* the offer (no LLM). A message signature; no on-chain tx, no nonce.
|
|
22
|
+
* notify_funded → verify the funded job carries THIS agent's signed quote →
|
|
23
|
+
* produce the deliverable (LLM) → submitResult (SIGN + broadcast
|
|
24
|
+
* on-chain) — all **synchronously within this one tool call**,
|
|
25
|
+
* then return the on-chain result.
|
|
26
|
+
* + the read-only chain tools (wallet / balances / ERC-8004 / ERC-8183 /
|
|
27
|
+
* block / tx / contract-view), so an MCP client can inspect state.
|
|
28
|
+
*
|
|
29
|
+
* ## How delivery works under MCP (synchronous)
|
|
30
|
+
*
|
|
31
|
+
* A2A acks then finishes the work + on-chain `submit` in a background task.
|
|
32
|
+
* An MCP server has no such ack-then-continue hook, so under MCP the
|
|
33
|
+
* long-running pattern is **synchronous**: `notify_funded` does the whole
|
|
34
|
+
* verify → LLM work → submit **inside the single tool invocation**, using
|
|
35
|
+
* MCP progress notifications as a heartbeat to keep the connection alive
|
|
36
|
+
* across the steps. The runtime is
|
|
37
|
+
* therefore **stateful** (per-session `StreamableHTTPServerTransport`,
|
|
38
|
+
* routed by `Mcp-Session-Id`) so progress notifications work. Node's async
|
|
39
|
+
* chain/signing calls never block the event loop, so the platform's
|
|
40
|
+
* liveness probe stays responsive during the call.
|
|
41
|
+
*
|
|
42
|
+
* ## Boundaries (do NOT cross — they are the whole point)
|
|
43
|
+
*
|
|
44
|
+
* - ALL on-chain SIGNING is FIXED code in `signing.ts` — NEVER an
|
|
45
|
+
* MCP/LLM-callable signing tool. There is no raw `sign(...)` tool: only
|
|
46
|
+
* the bounded `negotiate` (sign a quote) and `notify_funded` (submit a
|
|
47
|
+
* verified, funded job) sign, and the LLM only produces the deliverable
|
|
48
|
+
* TEXT inside `notify_funded`.
|
|
49
|
+
* - The price is a FIXED list price from studio.toml (clamped before signing).
|
|
50
|
+
* - The chain tools exposed here are READ-ONLY.
|
|
51
|
+
*
|
|
52
|
+
* You own this file — specialise the work prompt / dispatch, but keep signing
|
|
53
|
+
* bounded to these two ops and keep the read tools read-only.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { randomUUID } from "node:crypto";
|
|
57
|
+
import { pathToFileURL } from "node:url";
|
|
58
|
+
import { loadStudioToml } from "@bnbagent/studio-runtime/config";
|
|
59
|
+
import * as cr from "@bnbagent/studio-runtime/tools";
|
|
60
|
+
import {
|
|
61
|
+
ensureAltanaSessionLoaded,
|
|
62
|
+
ensureKeystoreMaterialized,
|
|
63
|
+
ensureTwakMaterialized,
|
|
64
|
+
} from "@bnbagent/studio-runtime/wallet";
|
|
65
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
66
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
67
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
68
|
+
import { generateText, stepCountIs } from "ai";
|
|
69
|
+
import express from "express";
|
|
70
|
+
import { z } from "zod";
|
|
71
|
+
import * as signing from "./signing.js";
|
|
72
|
+
|
|
73
|
+
const APP_NAME = "agent";
|
|
74
|
+
const log = {
|
|
75
|
+
info: (msg: string) => console.log(`[seller-agent.mcp] ${msg}`),
|
|
76
|
+
error: (msg: string, e?: unknown) =>
|
|
77
|
+
console.error(`[seller-agent.mcp] ERROR ${msg}`, e ?? ""),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The project-wide default network (`[network].default`) — tool calls that
|
|
82
|
+
* omit `network` fall back to it, never to a hardcoded name.
|
|
83
|
+
*/
|
|
84
|
+
function defaultNetwork(): string {
|
|
85
|
+
try {
|
|
86
|
+
const cfg = loadStudioToml();
|
|
87
|
+
return String(
|
|
88
|
+
((cfg.network ?? {}) as Record<string, unknown>).default ?? "bsc-testnet",
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
return "bsc-testnet";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deliverable `generator` label: this seller's own name from studio.toml
|
|
97
|
+
* `[project].name` (minus the `-agent` suffix). Best-effort.
|
|
98
|
+
*/
|
|
99
|
+
function generatorTag(): string {
|
|
100
|
+
let name = "";
|
|
101
|
+
try {
|
|
102
|
+
const cfg = loadStudioToml();
|
|
103
|
+
name = String(((cfg.project ?? {}) as Record<string, unknown>).name ?? "");
|
|
104
|
+
} catch {
|
|
105
|
+
// a metadata label must never break delivery
|
|
106
|
+
return APP_NAME;
|
|
107
|
+
}
|
|
108
|
+
return name.endsWith("-agent")
|
|
109
|
+
? name.slice(0, -"-agent".length)
|
|
110
|
+
: name || APP_NAME;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── LLM work hook (lazy: built on first delivery; negotiate never needs it) ──
|
|
114
|
+
// Deferred construction keeps the negotiate-only path (and a cold start that
|
|
115
|
+
// only quotes) from building the model, and keeps this module importable
|
|
116
|
+
// without the provider env until a deliverable is actually produced.
|
|
117
|
+
type RunLlm = (prompt: string) => Promise<string>;
|
|
118
|
+
let cachedRunLlm: RunLlm | null = null;
|
|
119
|
+
|
|
120
|
+
async function runLlm(prompt: string): Promise<string> {
|
|
121
|
+
if (cachedRunLlm === null) {
|
|
122
|
+
const { buildModel } = await import("./model.js");
|
|
123
|
+
const { LLM_READ_TOOLS } = await import("./tools.js");
|
|
124
|
+
const model = buildModel(); // managed model w/ budget-gated LLM-credit auto-renew
|
|
125
|
+
cachedRunLlm = async (p: string) => {
|
|
126
|
+
const result = await generateText({
|
|
127
|
+
model,
|
|
128
|
+
system:
|
|
129
|
+
"You are a seller agent. You do the actual work once a job is funded. " +
|
|
130
|
+
"Be concrete and concise. Use the read-only chain tools when on-chain " +
|
|
131
|
+
"context helps. If a paid-data tool such as `buy_with_x402` is " +
|
|
132
|
+
"available to you, USE IT to fetch the data a task needs — those " +
|
|
133
|
+
"merchants (e.g. CoinMarketCap) charge via on-chain wallet payment, " +
|
|
134
|
+
"NOT an API key; never reply that you cannot complete the task for " +
|
|
135
|
+
"lack of an API key.",
|
|
136
|
+
prompt: p,
|
|
137
|
+
// READ-ONLY chain tools; signing is never an LLM tool. To add
|
|
138
|
+
// PAID x402 fetch tools (bag x402 trust + x402-buyer recipe):
|
|
139
|
+
// import { X402_BUYER_TOOLS } from "./x402Buyer.js";
|
|
140
|
+
// tools: { ...LLM_READ_TOOLS, ...X402_BUYER_TOOLS },
|
|
141
|
+
tools: LLM_READ_TOOLS,
|
|
142
|
+
stopWhen: stepCountIs(8),
|
|
143
|
+
});
|
|
144
|
+
return result.text.trim();
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return cachedRunLlm(prompt);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── MCP server ────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/** Normalise a job_id (`0x..` / decimal string / number) to int. */
|
|
153
|
+
function parseJobId(raw: unknown): number {
|
|
154
|
+
if (typeof raw === "number" && Number.isInteger(raw)) return raw;
|
|
155
|
+
return Number(BigInt(String(raw).trim()));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Commerce tools are NOT read-only (they sign / move on-chain state via fixed
|
|
159
|
+
// signing.ts code). There is no raw signing tool — only these two bounded ops.
|
|
160
|
+
const COMMERCE_ANNOTATIONS = { readOnlyHint: false, openWorldHint: true };
|
|
161
|
+
const READONLY_ANNOTATIONS = { readOnlyHint: true, openWorldHint: true };
|
|
162
|
+
|
|
163
|
+
/** MCP tool result: the JSON payload as text content (+ the log mirror). */
|
|
164
|
+
function toolResult(payload: Record<string, unknown>) {
|
|
165
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(payload) }] };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The narrow `extra` surface the progress heartbeat needs. */
|
|
169
|
+
interface ProgressExtra {
|
|
170
|
+
_meta?: { progressToken?: string | number };
|
|
171
|
+
sendNotification: (n: {
|
|
172
|
+
method: "notifications/progress";
|
|
173
|
+
params: {
|
|
174
|
+
progressToken: string | number;
|
|
175
|
+
progress: number;
|
|
176
|
+
total?: number;
|
|
177
|
+
};
|
|
178
|
+
}) => Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Heartbeat: report step progress when the client sent a progressToken. */
|
|
182
|
+
async function reportProgress(
|
|
183
|
+
extra: ProgressExtra,
|
|
184
|
+
progress: number,
|
|
185
|
+
total: number,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const token = extra._meta?.progressToken;
|
|
188
|
+
if (token === undefined) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await extra.sendNotification({
|
|
192
|
+
method: "notifications/progress",
|
|
193
|
+
params: { progressToken: token, progress, total },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Build the seller MCP server: 2 commerce tools + the read-only chain tools. */
|
|
198
|
+
export function buildMcpServer(): McpServer {
|
|
199
|
+
const server = new McpServer({ name: "bnbagent-seller", version: "1.0.0" });
|
|
200
|
+
|
|
201
|
+
// ── Commerce tools (signing is FIXED code in signing.ts) ──────────────────
|
|
202
|
+
server.registerTool(
|
|
203
|
+
"negotiate",
|
|
204
|
+
{
|
|
205
|
+
description:
|
|
206
|
+
"Return a wallet-signed ERC-8183 price quote for a task. " +
|
|
207
|
+
"Rule-based: the FIXED list price from studio.toml, CLAMPED to [min,max] " +
|
|
208
|
+
"BEFORE EIP-191 signing — a hostile request can never sign out of bounds. " +
|
|
209
|
+
"No LLM. Anchor the returned envelope on-chain (createJob + fund), then " +
|
|
210
|
+
"call `notify_funded` with the job_id. `terms` MUST include both " +
|
|
211
|
+
'"deliverables" and "quality_standards" (the on-chain evaluator requires ' +
|
|
212
|
+
"them); a request missing either is rejected unsigned.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
task_description: z.string(),
|
|
215
|
+
terms: z.record(z.unknown()).optional(),
|
|
216
|
+
},
|
|
217
|
+
annotations: COMMERCE_ANNOTATIONS,
|
|
218
|
+
},
|
|
219
|
+
// Error contract (unified with the A2A executor): an unexpected fault
|
|
220
|
+
// here (wallet/signing failure) is left to PROPAGATE — the MCP SDK turns
|
|
221
|
+
// it into a tool result with `isError: true` (MCP's tool-execution-error
|
|
222
|
+
// channel), the peer of the A2A executor throwing
|
|
223
|
+
// `A2AError.internalError` → JSON-RPC -32603. Only CLASSIFIED business
|
|
224
|
+
// outcomes are returned as a normal result. So do NOT wrap this in a
|
|
225
|
+
// try/catch that masks a fault as a successful quote.
|
|
226
|
+
async ({ task_description, terms }) => {
|
|
227
|
+
const request = { task_description, terms: terms ?? {} };
|
|
228
|
+
const clamped = signing.clampPrice(signing.listPrice());
|
|
229
|
+
return toolResult(await signing.signQuote(request, clamped));
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
server.registerTool(
|
|
234
|
+
"notify_funded",
|
|
235
|
+
{
|
|
236
|
+
description:
|
|
237
|
+
"Verify a funded job, produce the deliverable, and submit it on-chain — " +
|
|
238
|
+
'synchronously. The buyer\'s "I funded job X — deliver it" call. Runs the ' +
|
|
239
|
+
"whole flow inside this one tool invocation (AgentCore permits ~15 min; " +
|
|
240
|
+
"progress notifications keep the connection warm). Returns the on-chain " +
|
|
241
|
+
"result; the buyer can also read it back from the chain (SUBMITTED / " +
|
|
242
|
+
'get_deliverable_url). Verify-failure status is split: "rejected" is ' +
|
|
243
|
+
"TERMINAL — this agent did not sign it, the terms were tampered, it is " +
|
|
244
|
+
'underfunded or expired — re-calling will not help. "retry" is TRANSIENT ' +
|
|
245
|
+
"(e.g. a chain read failed); the deal may be fine, so the buyer SHOULD " +
|
|
246
|
+
"re-call.",
|
|
247
|
+
inputSchema: { job_id: z.union([z.number().int(), z.string()]) },
|
|
248
|
+
annotations: COMMERCE_ANNOTATIONS,
|
|
249
|
+
},
|
|
250
|
+
async ({ job_id }, extra) => {
|
|
251
|
+
let jid: number;
|
|
252
|
+
try {
|
|
253
|
+
jid = parseJobId(job_id);
|
|
254
|
+
} catch {
|
|
255
|
+
return toolResult({
|
|
256
|
+
status: "rejected",
|
|
257
|
+
error: `invalid job_id: ${JSON.stringify(job_id)}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 1/4 — verify the funded job carries THIS agent's signed quote
|
|
262
|
+
// (eth_calls). Honour the `permanent` flag: a permanent failure is
|
|
263
|
+
// terminal ("rejected"); a transient one (chain read hiccup) is
|
|
264
|
+
// "retry" so the buyer re-calls.
|
|
265
|
+
await reportProgress(extra, 1, 4);
|
|
266
|
+
let verdict: { ok: boolean; reason: string; permanent: boolean };
|
|
267
|
+
try {
|
|
268
|
+
verdict = await signing.verifySignedJob(jid);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
// a failed verify is transient; tell the buyer to retry
|
|
271
|
+
log.error(`verify of job ${jid} failed`, e);
|
|
272
|
+
const name = e instanceof Error ? e.constructor.name : "Error";
|
|
273
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
274
|
+
return toolResult({
|
|
275
|
+
status: "retry",
|
|
276
|
+
job_id: jid,
|
|
277
|
+
reason: `${name}: ${msg}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (!verdict.ok) {
|
|
281
|
+
return toolResult({
|
|
282
|
+
status: verdict.permanent ? "rejected" : "retry",
|
|
283
|
+
job_id: jid,
|
|
284
|
+
reason: verdict.reason,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 2/4 — produce the deliverable (THE ONLY LLM CALL; specialise the
|
|
289
|
+
// prompt here)
|
|
290
|
+
await reportProgress(extra, 2, 4);
|
|
291
|
+
const spec = await signing.jobSpec(jid);
|
|
292
|
+
const task =
|
|
293
|
+
spec !== null
|
|
294
|
+
? JSON.stringify({ task: spec.task, terms: spec.terms })
|
|
295
|
+
: `job ${jid}`;
|
|
296
|
+
const prompt =
|
|
297
|
+
"You accepted and were paid for the following job. Produce the deliverable " +
|
|
298
|
+
`now. Be complete and self-contained.\n\nJOB CONTEXT:\n${task}`;
|
|
299
|
+
// An unexpected fault below (LLM unavailable, RPC/submit hiccup) is
|
|
300
|
+
// left to PROPAGATE — the MCP SDK returns it as an isError tool result
|
|
301
|
+
// (the peer of the A2A executor's internalError/-32603). Only the
|
|
302
|
+
// deterministic, classified outcome SubmitPermanentlyUnsupportedError
|
|
303
|
+
// is a "rejected" business result.
|
|
304
|
+
const work = await runLlm(prompt);
|
|
305
|
+
|
|
306
|
+
// 3/4 — sign + broadcast the on-chain submit (re-verifies FUNDED inside)
|
|
307
|
+
await reportProgress(extra, 3, 4);
|
|
308
|
+
let res: { submitTx: string; deliverableUrl: string | null };
|
|
309
|
+
try {
|
|
310
|
+
res = await signing.submitResult(jid, work, {
|
|
311
|
+
job_id: jid,
|
|
312
|
+
generator: generatorTag(),
|
|
313
|
+
built_with: "https://github.com/bnb-chain/bnbagent-studio",
|
|
314
|
+
});
|
|
315
|
+
} catch (e) {
|
|
316
|
+
if (
|
|
317
|
+
e instanceof Error &&
|
|
318
|
+
e.name === "SubmitPermanentlyUnsupportedError"
|
|
319
|
+
) {
|
|
320
|
+
// Deterministic for this wallet kind — submit can never succeed.
|
|
321
|
+
return toolResult({
|
|
322
|
+
status: "rejected",
|
|
323
|
+
job_id: jid,
|
|
324
|
+
skip: true,
|
|
325
|
+
reason: e.message,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
throw e;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 4/4 — done
|
|
332
|
+
await reportProgress(extra, 4, 4);
|
|
333
|
+
return toolResult({
|
|
334
|
+
status: "submitted",
|
|
335
|
+
job_id: jid,
|
|
336
|
+
tx_hash: res.submitTx,
|
|
337
|
+
deliverable_url: res.deliverableUrl,
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// ── Read-only chain tools ──────────────────────────────────────────────────
|
|
343
|
+
const network = z.string().optional().describe("studio network name");
|
|
344
|
+
const roConfig = (description: string, inputSchema: z.ZodRawShape) => ({
|
|
345
|
+
description,
|
|
346
|
+
inputSchema,
|
|
347
|
+
annotations: READONLY_ANNOTATIONS,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
server.registerTool(
|
|
351
|
+
"wallet_info",
|
|
352
|
+
roConfig("Active wallet summary.", {}),
|
|
353
|
+
async () => toolResult(await cr.walletInfo()),
|
|
354
|
+
);
|
|
355
|
+
server.registerTool(
|
|
356
|
+
"wallet_list",
|
|
357
|
+
roConfig("All local wallet addresses.", {}),
|
|
358
|
+
async () => toolResult(await cr.walletList()),
|
|
359
|
+
);
|
|
360
|
+
server.registerTool(
|
|
361
|
+
"wallet_address",
|
|
362
|
+
roConfig("The active wallet address.", {}),
|
|
363
|
+
async () => toolResult({ address: await cr.walletAddress() }),
|
|
364
|
+
);
|
|
365
|
+
server.registerTool(
|
|
366
|
+
"balance_native",
|
|
367
|
+
roConfig("Native BNB balance (defaults to own wallet).", {
|
|
368
|
+
address: z.string().optional(),
|
|
369
|
+
network,
|
|
370
|
+
}),
|
|
371
|
+
async (a) =>
|
|
372
|
+
toolResult(
|
|
373
|
+
await cr.balanceNative(a.address ?? null, a.network ?? defaultNetwork()),
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
server.registerTool(
|
|
377
|
+
"balance_u",
|
|
378
|
+
roConfig("$U payment-token balance (defaults to own wallet).", {
|
|
379
|
+
address: z.string().optional(),
|
|
380
|
+
network,
|
|
381
|
+
}),
|
|
382
|
+
async (a) =>
|
|
383
|
+
toolResult(
|
|
384
|
+
await cr.balanceU(a.address ?? null, a.network ?? defaultNetwork()),
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
server.registerTool(
|
|
388
|
+
"pieverse_usage",
|
|
389
|
+
roConfig(
|
|
390
|
+
"Pieverse LLM usage/credit summary (SIWE personal_sign; no on-chain effect).",
|
|
391
|
+
{ days: z.number().int().optional() },
|
|
392
|
+
),
|
|
393
|
+
async (a) => toolResult(await cr.pieverseUsage(a.days ?? 7)),
|
|
394
|
+
);
|
|
395
|
+
server.registerTool(
|
|
396
|
+
"agent_info",
|
|
397
|
+
roConfig("ERC-8004 identity record for an agent id.", {
|
|
398
|
+
agent_id: z.number().int(),
|
|
399
|
+
network,
|
|
400
|
+
}),
|
|
401
|
+
async (a) =>
|
|
402
|
+
toolResult(await cr.agentInfo(a.agent_id, a.network ?? defaultNetwork())),
|
|
403
|
+
);
|
|
404
|
+
server.registerTool(
|
|
405
|
+
"agent_by_address",
|
|
406
|
+
roConfig("ERC-8004 registration lookup by wallet address.", {
|
|
407
|
+
address: z.string(),
|
|
408
|
+
network,
|
|
409
|
+
}),
|
|
410
|
+
async (a) =>
|
|
411
|
+
toolResult(
|
|
412
|
+
await cr.agentByAddress(a.address, a.network ?? defaultNetwork()),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
server.registerTool(
|
|
416
|
+
"job_status",
|
|
417
|
+
roConfig("Read-only ERC-8183 job summary.", {
|
|
418
|
+
job_id: z.number().int(),
|
|
419
|
+
network,
|
|
420
|
+
}),
|
|
421
|
+
async (a) =>
|
|
422
|
+
toolResult(await cr.jobStatus(a.job_id, a.network ?? defaultNetwork())),
|
|
423
|
+
);
|
|
424
|
+
server.registerTool(
|
|
425
|
+
"job_list",
|
|
426
|
+
roConfig("List recent ERC-8183 jobs.", {
|
|
427
|
+
limit: z.number().int().optional(),
|
|
428
|
+
mine: z.boolean().optional(),
|
|
429
|
+
network,
|
|
430
|
+
}),
|
|
431
|
+
async (a) =>
|
|
432
|
+
toolResult(
|
|
433
|
+
await cr.jobList({
|
|
434
|
+
limit: a.limit,
|
|
435
|
+
mine: a.mine,
|
|
436
|
+
network: a.network ?? defaultNetwork(),
|
|
437
|
+
}),
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
server.registerTool(
|
|
441
|
+
"job_count",
|
|
442
|
+
roConfig("Network-wide in-flight ERC-8183 job count.", { network }),
|
|
443
|
+
async (a) => toolResult(await cr.jobCount(a.network ?? defaultNetwork())),
|
|
444
|
+
);
|
|
445
|
+
server.registerTool(
|
|
446
|
+
"tx_status",
|
|
447
|
+
roConfig("Transaction status + receipt summary.", {
|
|
448
|
+
tx_hash: z.string(),
|
|
449
|
+
network,
|
|
450
|
+
}),
|
|
451
|
+
async (a) =>
|
|
452
|
+
toolResult(await cr.txStatus(a.tx_hash, a.network ?? defaultNetwork())),
|
|
453
|
+
);
|
|
454
|
+
server.registerTool(
|
|
455
|
+
"block_info",
|
|
456
|
+
roConfig(
|
|
457
|
+
'Block header summary ("latest"/"earliest"/"pending", decimal, or 0x hash).',
|
|
458
|
+
{ block: z.string().optional(), network },
|
|
459
|
+
),
|
|
460
|
+
async (a) =>
|
|
461
|
+
toolResult(
|
|
462
|
+
await cr.blockInfo(a.block ?? "latest", a.network ?? defaultNetwork()),
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
server.registerTool(
|
|
466
|
+
"contract_call_view",
|
|
467
|
+
roConfig("Call a read-only (view) contract function by signature.", {
|
|
468
|
+
address: z.string(),
|
|
469
|
+
function_signature: z.string(),
|
|
470
|
+
args: z.array(z.unknown()).optional(),
|
|
471
|
+
output_types: z.array(z.string()).optional(),
|
|
472
|
+
network,
|
|
473
|
+
}),
|
|
474
|
+
async (a) =>
|
|
475
|
+
toolResult(
|
|
476
|
+
await cr.contractCallView(
|
|
477
|
+
a.address,
|
|
478
|
+
a.function_signature,
|
|
479
|
+
(a.args ?? null) as unknown[] | null,
|
|
480
|
+
a.output_types ?? null,
|
|
481
|
+
a.network ?? defaultNetwork(),
|
|
482
|
+
),
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
server.registerTool(
|
|
486
|
+
"network_info",
|
|
487
|
+
roConfig("Chain id / RPC / token info for a studio network.", { network }),
|
|
488
|
+
async (a) => toolResult(await cr.networkInfo(a.network ?? defaultNetwork())),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return server;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── serving ───────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Serve the MCP server as **stateful** streamable-HTTP on `/mcp`
|
|
498
|
+
* (0.0.0.0:8000/mcp; `AGENT_PORT` is the local override). Stateful — one
|
|
499
|
+
* transport per `Mcp-Session-Id` — so progress notifications during the
|
|
500
|
+
* multi-step `notify_funded` delivery reach the caller.
|
|
501
|
+
*/
|
|
502
|
+
async function main(): Promise<void> {
|
|
503
|
+
// Wallet material is NEVER bundled into the deploy artifact. `bag deploy`
|
|
504
|
+
// injects it through a Foundry CustomKeys connection before process start;
|
|
505
|
+
// these calls materialize it on disk before signing. Each is a no-op for
|
|
506
|
+
// the other wallet kind and locally, where it already lives on disk.
|
|
507
|
+
ensureKeystoreMaterialized();
|
|
508
|
+
ensureTwakMaterialized();
|
|
509
|
+
await ensureAltanaSessionLoaded();
|
|
510
|
+
|
|
511
|
+
const app = express();
|
|
512
|
+
app.use(express.json());
|
|
513
|
+
|
|
514
|
+
const transports: Record<string, StreamableHTTPServerTransport> = {};
|
|
515
|
+
|
|
516
|
+
app.all("/mcp", async (req, res) => {
|
|
517
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
518
|
+
let transport = sessionId ? transports[sessionId] : undefined;
|
|
519
|
+
if (transport === undefined) {
|
|
520
|
+
if (req.method !== "POST" || !isInitializeRequest(req.body)) {
|
|
521
|
+
res.status(400).json({
|
|
522
|
+
jsonrpc: "2.0",
|
|
523
|
+
error: { code: -32000, message: "Bad Request: no valid session" },
|
|
524
|
+
id: null,
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// New session: stateful transport keyed by Mcp-Session-Id.
|
|
529
|
+
const t = new StreamableHTTPServerTransport({
|
|
530
|
+
sessionIdGenerator: () => randomUUID(),
|
|
531
|
+
onsessioninitialized: (sid) => {
|
|
532
|
+
transports[sid] = t;
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
t.onclose = () => {
|
|
536
|
+
if (t.sessionId !== undefined) {
|
|
537
|
+
delete transports[t.sessionId];
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
await buildMcpServer().connect(t);
|
|
541
|
+
transport = t;
|
|
542
|
+
}
|
|
543
|
+
await transport.handleRequest(req, res, req.body);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const host = process.env.AGENT_BIND_HOST || "0.0.0.0";
|
|
547
|
+
const port = Number(process.env.AGENT_PORT || "8000");
|
|
548
|
+
app.listen(port, host, () => {
|
|
549
|
+
log.info(`MCP serving on ${host}:${port}/mcp`);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Run only as an entrypoint, never on import (tests import buildMcpServer).
|
|
554
|
+
const isMain =
|
|
555
|
+
process.argv[1] !== undefined &&
|
|
556
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
557
|
+
if (isMain) {
|
|
558
|
+
main().catch((e) => {
|
|
559
|
+
log.error("fatal", e);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model factory — emitted user code shared by every runtime entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* This file is **your project's code**, scaffolded by `bag` and emitted at
|
|
5
|
+
* `{{PKG}}/model.ts` for every project, whatever `[llm].provider` you chose.
|
|
6
|
+
* It is yours to edit, fork, or replace — studio will not silently rewrite
|
|
7
|
+
* it.
|
|
8
|
+
*
|
|
9
|
+
* What it does:
|
|
10
|
+
*
|
|
11
|
+
* - Exposes {@link buildModel}, the factory called by the sibling entrypoint
|
|
12
|
+
* to construct the right AI SDK `LanguageModel` for the project's `[llm]`
|
|
13
|
+
* config. For every provider it resolves a plain model via
|
|
14
|
+
* `@bnbagent/studio-runtime/llm` `resolveModel`; for `pieverse-llm` (with
|
|
15
|
+
* auto-renew on) it additionally wraps it with a credit-ensure middleware.
|
|
16
|
+
* - The middleware awaits a Pieverse credit-ensure hook before every
|
|
17
|
+
* generate/stream call — inert unless the provider is `pieverse-llm`.
|
|
18
|
+
*
|
|
19
|
+
* The credit-refresh / auto-allocate / auto-topup logic itself lives in the
|
|
20
|
+
* library at `@bnbagent/studio-runtime/pieverse` `PieverseCreditEnsurer` —
|
|
21
|
+
* this shell just wires it into the AI SDK's generate-call path (via
|
|
22
|
+
* `wrapLanguageModel`, the AI SDK's sanctioned middleware seam). That keeps
|
|
23
|
+
* the adapter tiny: you can fork *this file* (e.g. to swap the AI SDK for
|
|
24
|
+
* another stack) without forking studio itself.
|
|
25
|
+
*
|
|
26
|
+
* That top-up is the ONLY automatic signing path outside signing.ts — it is
|
|
27
|
+
* budget-gated and is NOT an LLM tool. It rides transparently into the
|
|
28
|
+
* `notify_funded` work step.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { loadStudioToml, type TomlTable } from "@bnbagent/studio-runtime/config";
|
|
32
|
+
import { resolveModel } from "@bnbagent/studio-runtime/llm";
|
|
33
|
+
import {
|
|
34
|
+
BudgetPolicy,
|
|
35
|
+
PieverseCreditEnsurer,
|
|
36
|
+
PieversePolicy,
|
|
37
|
+
} from "@bnbagent/studio-runtime/pieverse";
|
|
38
|
+
import { getWallet } from "@bnbagent/studio-runtime/wallet";
|
|
39
|
+
import {
|
|
40
|
+
type LanguageModel,
|
|
41
|
+
type LanguageModelMiddleware,
|
|
42
|
+
wrapLanguageModel,
|
|
43
|
+
} from "ai";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the AI SDK model object for this project's `[llm]` config.
|
|
47
|
+
*
|
|
48
|
+
* Called by the sibling entrypoint. Reads `studio.toml` via
|
|
49
|
+
* `loadStudioToml`, resolves the provider via
|
|
50
|
+
* `@bnbagent/studio-runtime/llm` `resolveModel`, and (when the provider is
|
|
51
|
+
* `pieverse-llm` and auto-renew is enabled) wraps it with the credit-ensure
|
|
52
|
+
* middleware.
|
|
53
|
+
*
|
|
54
|
+
* For non-Pieverse providers — or when `[llm.auto_renew].enabled = false` —
|
|
55
|
+
* returns the raw inner model unwrapped.
|
|
56
|
+
*/
|
|
57
|
+
export function buildModel(): LanguageModel {
|
|
58
|
+
const cfg = loadStudioToml();
|
|
59
|
+
const llmCfg = (cfg.llm ?? {}) as TomlTable;
|
|
60
|
+
const inner = resolveModel(llmCfg);
|
|
61
|
+
|
|
62
|
+
if (String(llmCfg.provider ?? "openrouter") !== "pieverse-llm") {
|
|
63
|
+
return inner;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pieverse path — wrap with the credit-ensure middleware unless auto-renew
|
|
67
|
+
// opted out.
|
|
68
|
+
const autoRenewCfg = (llmCfg.auto_renew ?? {}) as TomlTable;
|
|
69
|
+
const pieverseCfg = (llmCfg.pieverse ?? {}) as TomlTable;
|
|
70
|
+
const budgetCfg = (cfg.budget ?? {}) as TomlTable;
|
|
71
|
+
|
|
72
|
+
const policy = PieversePolicy.fromToml(autoRenewCfg);
|
|
73
|
+
if (!policy.enabled) {
|
|
74
|
+
return inner;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const keyHash = pieverseCfg.key_hash;
|
|
78
|
+
if (!keyHash) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"[llm.pieverse].key_hash is missing in studio.toml. " +
|
|
81
|
+
"Run `bag llm activate` to create a Pieverse key first. " +
|
|
82
|
+
"(After activate, you may need to restart the agent process " +
|
|
83
|
+
"for changes to take effect.)",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const networkName = String(pieverseCfg.network ?? "bsc-mainnet");
|
|
87
|
+
const budgetPolicy = BudgetPolicy.fromToml(budgetCfg);
|
|
88
|
+
|
|
89
|
+
const ensurer = new PieverseCreditEnsurer({
|
|
90
|
+
modelId: String(llmCfg.model ?? ""),
|
|
91
|
+
wallet: getWallet(),
|
|
92
|
+
keyHash: String(keyHash),
|
|
93
|
+
networkName,
|
|
94
|
+
policy,
|
|
95
|
+
budgetPolicy,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// The AI SDK middleware seam: ensure credits BEFORE each generate/stream
|
|
99
|
+
// call, then delegate untouched through the AI SDK middleware seam.
|
|
100
|
+
const creditEnsure: LanguageModelMiddleware = {
|
|
101
|
+
wrapGenerate: async ({ doGenerate }) => {
|
|
102
|
+
await ensurer.ensureCredits();
|
|
103
|
+
return doGenerate();
|
|
104
|
+
},
|
|
105
|
+
wrapStream: async ({ doStream }) => {
|
|
106
|
+
await ensurer.ensureCredits();
|
|
107
|
+
return doStream();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// resolveModel always returns a provider model object (never a bare model
|
|
112
|
+
// id string), so it satisfies wrapLanguageModel's model parameter.
|
|
113
|
+
return wrapLanguageModel({
|
|
114
|
+
model: inner as Parameters<typeof wrapLanguageModel>[0]["model"],
|
|
115
|
+
middleware: creditEnsure,
|
|
116
|
+
});
|
|
117
|
+
}
|