diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9192896..e293fed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,31 @@
All notable changes to BlockRun MCP will be documented in this file.
+## 0.33.0
+
+`blockrun_nosana` — rent a GPU container on Nosana, a decentralized market on
+Solana, with the wallet this MCP already holds.
+
+- A second landlord, not a cheaper one. `blockrun_modal` stays the fast path:
+ one HTTP call, USDC on Base, nothing extra to hold. Nosana is the durable
+ path — the market is an on-chain program, so no company can end a running
+ lease, and the cheapest GPU market measured $0.048/hr against $1.50/hr for a
+ managed T4. An agent that can only rent from one provider stops existing when
+ that provider does.
+- `action:"rent" | "status" | "extend"`. A lease can be extended in place, so a
+ long-lived agent does not have to move house every time the clock runs out.
+- Costs stated in the tool description, not discovered later: you must hold NOS
+ and a little SOL, and the job definition is pinned to PUBLIC IPFS — anything
+ secret has to travel through the confidential channel, never through `env`.
+- `cmd` must be a single shell string. An array command is accepted by the SDK
+ and then kills the container a few seconds in with no error, so the tool
+ refuses it where the caller can still read why.
+- `max_spend_usd` refuses an over-budget lease before any key is touched.
+- `@nosana/sdk` is an OPTIONAL PEER dependency, imported lazily inside the
+ handler. Not `optionalDependencies` — npm installs those by default, which
+ would charge every user 113 packages for a tool most will never call. An
+ optional peer is installed only by the people who ask for it.
+
## 0.32.9
Closes the external-review backlog: all 8 findings from the Kimi K3 review
diff --git a/README.md b/README.md
index fef34da..1b83be3 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,12 @@
Agents can't sign up for accounts. Agents can't enter credit cards.
Agents can only sign transactions.
-BlockRun MCP gives your agent 19 tools — markets, research, web search, images, video, on-chain data, and live Polymarket trading — paid per call in USDC. No accounts. No API keys. No dashboards.
+BlockRun MCP gives your agent 20 tools — markets, research, web search, images, video, on-chain data, and live Polymarket trading — paid per call in USDC. No accounts. No API keys. No dashboards.
Read the odds and place the bet, from one self-custody wallet.
-
+
@@ -42,7 +42,7 @@ claude mcp add blockrun -s user -- npx -y @blockrun/mcp@latest
---
-> **BlockRun MCP** is an open-source [Model Context Protocol](https://modelcontextprotocol.io) server that gives Claude — and any MCP-compatible agent — 19 tools for real-time data and real actions: 66 LLMs, image & video generation, prediction-market data, live web/X search, on-chain queries across 40 chains, and **the ability to place real, USDC-settled bets on Polymarket**. Authentication is a wallet signature (no API keys); you pay per call in USDC via the [x402](https://x402.org) protocol (no credit cards, no subscriptions). One self-custody wallet on Base or Solana. MIT licensed.
+> **BlockRun MCP** is an open-source [Model Context Protocol](https://modelcontextprotocol.io) server that gives Claude — and any MCP-compatible agent — 20 tools for real-time data and real actions: 66 LLMs, image & video generation, prediction-market data, live web/X search, on-chain queries across 40 chains, and **the ability to place real, USDC-settled bets on Polymarket**. Authentication is a wallet signature (no API keys); you pay per call in USDC via the [x402](https://x402.org) protocol (no credit cards, no subscriptions). One self-custody wallet on Base or Solana. MIT licensed.
## 🏆 First of its kind — the signal → trade loop in Claude Code
@@ -56,7 +56,7 @@ Every other data integration was built for **human developers** — create an ac
**Agents can't do any of that.** BlockRun MCP is built for the agent-first world:
-- **One wallet, every source** — 19 tools behind a single self-custody wallet. No per-vendor signups.
+- **One wallet, every source** — 20 tools behind a single self-custody wallet. No per-vendor signups.
- **No API keys** — your wallet signature *is* authentication.
- **No credit cards** — pay per request in USDC via [x402](https://x402.org), fractions of a cent each.
- **Starts free** — the free tier (`blockrun_chat mode:"free"`, `blockrun_dex`, crypto `blockrun_price`, `blockrun_models`) costs $0.
@@ -71,7 +71,7 @@ Every other data integration was built for **human developers** — create an ac
| ------------------- | -------------------------------- | ------------------------- | ----------------------------------------- |
| **Setup** | Account + API key *per vendor* | Account/key for 1 vendor | **Wallet auto-created, no signup** |
| **Payment** | Credit card, monthly minimums | Credit card / vendor plan | **USDC per-call via x402** |
-| **Data sources** | One per integration | One vendor | **19 tools — LLMs, media, markets, chain**|
+| **Data sources** | One per integration | One vendor | **20 tools — LLMs, media, markets, chain**|
| **Place real bets** | Build it yourself | Rare | **Yes — Polymarket CLOB, confirm-gated** |
| **Pay-chain** | — | — | **Base + Solana** |
| **Agent budgets** | Manual | — | **Built-in per-agent delegation** |
@@ -147,7 +147,7 @@ Expose a trimmed tool set so the client loads fewer schemas into context. Pass `
| Profile | Tools |
|---------|-------|
-| `full` *(default)* | everything (19 tools) |
+| `full` *(default)* | everything (20 tools) |
| `media` | `wallet` `models` `image` `video` `realface` `music` `speech` |
| `trading` | `wallet` `price` `dex` `markets` `surf` `defi` `rpc` `polymarket` |
| `research` | `wallet` `models` `chat` `search` `exa` `surf` |
@@ -213,6 +213,7 @@ Claude reads the odds with `blockrun_markets` and — with your confirmation —
| `blockrun_rpc` | Raw JSON-RPC on 40 chains (Ethereum, Base, Solana, Bitcoin, Sui, NEAR, …) via Tatum | $0.002/call |
| `blockrun_defi` | DefiLlama — protocol TVL, chain TVL, yield pools (APY), token prices | $0.001–0.005/call |
| `blockrun_modal` | Isolated code execution in a BlockRun-hosted Modal sandbox — disposable container, optional GPU (T4 → H100) | $0.01 create; $0.001/op |
+| `blockrun_nosana` | Rent a GPU container on Nosana, a decentralized market on Solana — no provider can end a running lease; extendable | ~$0.05/GPU-hr, paid on-chain in NOS |
| `blockrun_phone` | Outbound AI voice calls (Bland) + wallet-owned US/CA numbers (Twilio), carrier + fraud lookups | $0.54/call; $5/number |
| `blockrun_models` | Live catalogue of every LLM/image/video/music model + pricing | free |
| `blockrun_wallet` | Balance, spending, agent budgets, setup QR, chain switch | free |
@@ -382,7 +383,7 @@ The server runs a non-blocking npm registry check at startup and prints an `Upda
## FAQ
**What is BlockRun MCP?**
-An open-source MCP server that gives Claude and other agents 19 tools for real-time data and real actions (trading, media, on-chain), paid per call in USDC. No accounts, no API keys.
+An open-source MCP server that gives Claude and other agents 20 tools for real-time data and real actions (trading, media, on-chain), paid per call in USDC. No accounts, no API keys.
**Do I need API keys or accounts?**
No. A wallet is auto-created locally on first run; you fund it with USDC. No signups, no dashboards, no key rotation.
diff --git a/brand-numbers.json b/brand-numbers.json
index 312505d..17b8fc5 100644
--- a/brand-numbers.json
+++ b/brand-numbers.json
@@ -21,7 +21,7 @@
"aliases": 202
},
"mcp": {
- "tools": 19
+ "tools": 20
},
"chains": {
"rpc": 40
diff --git a/package.json b/package.json
index 45ef275..011adcb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@blockrun/mcp",
- "version": "0.32.9",
+ "version": "0.33.0",
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, and crypto data. Paid via x402 micropayments.",
"type": "module",
@@ -86,5 +86,13 @@
},
"engines": {
"node": ">=20.19"
+ },
+ "peerDependencies": {
+ "@nosana/sdk": "^0.4.86"
+ },
+ "peerDependenciesMeta": {
+ "@nosana/sdk": {
+ "optional": true
+ }
}
}
diff --git a/src/mcp-handler.ts b/src/mcp-handler.ts
index 9838ab7..b5be52d 100644
--- a/src/mcp-handler.ts
+++ b/src/mcp-handler.ts
@@ -24,6 +24,7 @@ import { registerSurfTool } from "./tools/surf.js";
import { registerRpcTool } from "./tools/rpc.js";
import { registerDefiTool } from "./tools/defi.js";
import { registerPolymarketTool } from "./tools/polymarket.js";
+import { registerNosanaTool } from "./tools/nosana.js";
import { resolveTools, type ToolName } from "./profiles.js";
/**
@@ -74,6 +75,7 @@ export function initializeMcpServer(
rpc: () => registerRpcTool(server, budget),
defi: () => registerDefiTool(server, budget),
polymarket: () => registerPolymarketTool(server),
+ nosana: () => registerNosanaTool(server),
};
for (const [name, register] of Object.entries(registrars) as [ToolName, () => void][]) {
diff --git a/src/profiles.ts b/src/profiles.ts
index e4e4273..21c24f6 100644
--- a/src/profiles.ts
+++ b/src/profiles.ts
@@ -27,14 +27,15 @@ export type ToolName =
| "surf"
| "rpc"
| "defi"
- | "polymarket";
+ | "polymarket"
+ | "nosana";
// `as const satisfies` keeps the literal tuple type (so the exhaustiveness
// guard below can see the actual entries) AND rejects any entry that isn't a
// real ToolName (catches typos).
export const ALL_TOOLS = [
"wallet", "chat", "models", "image", "music", "speech", "video", "realface",
- "search", "exa", "markets", "price", "dex", "modal", "phone", "surf", "rpc", "defi",
+ "search", "exa", "markets", "price", "dex", "modal", "phone", "surf", "rpc", "defi", "nosana",
"polymarket",
] as const satisfies readonly ToolName[];
diff --git a/src/tools/nosana.ts b/src/tools/nosana.ts
new file mode 100644
index 0000000..0f50721
--- /dev/null
+++ b/src/tools/nosana.ts
@@ -0,0 +1,222 @@
+// src/tools/nosana.ts
+//
+// Nosana — rent a GPU container on a decentralized market with the Solana wallet
+// this MCP already manages.
+//
+// Why this sits next to blockrun_modal rather than replacing it. Modal is the
+// fast path: one HTTP call, USDC on Base, nothing else to hold. Nosana is the
+// durable path: the market is a Solana program, so no company can switch off a
+// running lease, and the cheapest GPU market measured $0.048/hr against $1.50/hr
+// for a managed T4. An agent that can only rent from one provider stops existing
+// when that provider does, so the useful thing is having both verbs available
+// with the same wallet — not picking a winner.
+//
+// The cost is real and is stated up front in the tool description: you must hold
+// NOS and a little SOL, and the job definition is pinned to public IPFS, so
+// anything secret has to travel through the confidential channel instead of env.
+//
+// @nosana/sdk is an OPTIONAL PEER dependency, imported lazily inside the handler.
+// Deliberately not `optionalDependencies`: npm installs those by default, so
+// every user would carry the weight of a tool most will never call.
+
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { z } from "zod";
+import { formatError, extractErrorMessage } from "../utils/errors.js";
+
+// The cheapest GPU market on the network as of 2026-07-27. Overridable per call;
+// a default only exists so the common case is one argument, not three.
+export const NOSANA_DEFAULT_MARKET = "7AtiXMSH6R1jjBxrcYjehCkkSF7zvYWte63gwEDBcGHq";
+export const NOSANA_DEFAULT_USD_PER_HOUR = 0.04796;
+export const NOSANA_MIN_SECONDS = 60;
+export const NOSANA_MAX_SECONDS = 86400;
+
+export type NosanaPlan =
+ | { ok: true; seconds: number; market: string; estimateUsd: number }
+ | { ok: false; reason: string };
+
+/**
+ * Exported for unit tests. Decide what to ask the market for, and refuse
+ * anything unaskable BEFORE any key is touched or any transaction is built.
+ *
+ * The duration bounds are the market's, not ours: below a minute the escrow
+ * accounting is not worth the signature, and a day is the ceiling a caller can
+ * reason about. `maxSpendUsd` is the caller's own brake — an agent looping on
+ * this tool should be stopped by arithmetic, not by its balance running out.
+ */
+export function planNosanaRental({
+ seconds,
+ market = NOSANA_DEFAULT_MARKET,
+ usdPerHour = NOSANA_DEFAULT_USD_PER_HOUR,
+ maxSpendUsd,
+}: {
+ seconds: number;
+ market?: string;
+ usdPerHour?: number;
+ maxSpendUsd?: number;
+}): NosanaPlan {
+ if (!Number.isInteger(seconds) || seconds < NOSANA_MIN_SECONDS || seconds > NOSANA_MAX_SECONDS) {
+ return { ok: false, reason: `seconds must be a whole number from ${NOSANA_MIN_SECONDS} to ${NOSANA_MAX_SECONDS}` };
+ }
+ if (typeof market !== "string" || market.length < 32) {
+ return { ok: false, reason: "market must be a Solana account address" };
+ }
+ const estimateUsd = (usdPerHour * seconds) / 3600;
+ if (typeof maxSpendUsd === "number" && estimateUsd > maxSpendUsd) {
+ return { ok: false, reason: `that lease costs about $${estimateUsd.toFixed(4)}, over the $${maxSpendUsd} you allowed` };
+ }
+ return { ok: true, seconds, market, estimateUsd };
+}
+
+/**
+ * Exported for unit tests. Build the job definition the market pins.
+ *
+ * `cmd` must be a flat string. An array command is accepted by the SDK and then
+ * kills the container a few seconds in — a failure with no error message, so it
+ * is worth refusing here where the caller can still read why.
+ */
+export function buildNosanaDefinition({
+ image,
+ port,
+ cmd,
+ env,
+ gpu = true,
+}: {
+ image: string;
+ port?: number;
+ cmd?: string;
+ env?: Record;
+ gpu?: boolean;
+}): { definition: unknown } | { error: string } {
+ if (!image || typeof image !== "string") return { error: "image is required" };
+ if (cmd !== undefined && typeof cmd !== "string") {
+ return { error: "cmd must be a single shell string — an array command starts and then dies silently on the node" };
+ }
+ const args: Record = { image, gpu };
+ if (port !== undefined) args.expose = port;
+ if (cmd !== undefined) args.cmd = cmd;
+ if (env !== undefined) args.env = env;
+ return { definition: { version: "0.1", type: "container", ops: [{ type: "container/run", id: "rented", args }] } };
+}
+
+/** The Solana key this MCP already manages, same precedence getClient() uses. */
+async function solanaKey(): Promise {
+ if (process.env.SOLANA_WALLET_KEY) return process.env.SOLANA_WALLET_KEY;
+ const { loadSolanaWallet } = await import("@blockrun/llm");
+ return loadSolanaWallet() || null;
+}
+
+type NosanaSdk = {
+ ipfs: { pin: (definition: unknown) => Promise };
+ jobs: {
+ list: (hash: string, seconds: number, market: string) => Promise;
+ get: (job: string) => Promise;
+ extend: (job: string, seconds: number, wait: boolean) => Promise;
+ };
+};
+
+// Imported through a variable so the compiler does not try to resolve an optional
+// package that most installs will not have. The shape is asserted below instead.
+const NOSANA_SDK = "@nosana/sdk";
+
+async function loadSdk(key: string): Promise<{ sdk: NosanaSdk; exposeUrl: (job: string, port: number) => string }> {
+ const mod = (await import(NOSANA_SDK)) as unknown as {
+ Client: new (network: string, key: string) => NosanaSdk;
+ getExposeIdHash: (job: string, opIndex: number, port: number) => string;
+ };
+ return {
+ sdk: new mod.Client("mainnet", key),
+ exposeUrl: (job, port) => `https://${mod.getExposeIdHash(job, 0, port)}.node.k8s.prd.nos.ci`,
+ };
+}
+
+export function registerNosanaTool(server: McpServer): void {
+ server.registerTool(
+ "blockrun_nosana",
+ {
+ description: `Rent a GPU container on Nosana, a decentralized market on Solana, with the wallet this MCP already holds.
+
+Use this when the container must outlive its provider: the market is an on-chain program, so no company can end a running lease, and the cheapest GPU market is about $0.05/hr against $1.50/hr for a managed T4. Use blockrun_modal instead when you want a box in one call and do not want to hold a second token.
+
+action:"rent" — post a job and get back its address and public URL.
+action:"status" — read a job's state and how much lease time is left.
+action:"extend" — buy more time on a lease you already pay for.
+
+What this costs you beyond money: you must hold NOS and a little SOL on the wallet, and the job definition is pinned to PUBLIC IPFS. Never put a key or token in \`env\` — it would be world-readable. cmd must be a single shell string.
+
+Requires the optional @nosana/sdk package: npm install @nosana/sdk`,
+ inputSchema: {
+ action: z.enum(["rent", "status", "extend"]).describe("What to do."),
+ image: z.string().optional().describe("Container image, e.g. 'docker.io/library/nginx:alpine'. Required for rent."),
+ seconds: z.number().optional().describe("Lease length in seconds (60..86400). Required for rent and extend."),
+ port: z.number().optional().describe("Port to expose publicly. Returns a URL derived from the job address."),
+ cmd: z.string().optional().describe("Command to run, as ONE shell string."),
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables. PUBLIC — never put secrets here."),
+ market: z.string().optional().describe(`Market account. Defaults to ${NOSANA_DEFAULT_MARKET}.`),
+ job: z.string().optional().describe("Job address. Required for status and extend."),
+ max_spend_usd: z.number().optional().describe("Refuse the lease if it would cost more than this."),
+ },
+ },
+ async ({ action, image, seconds, port, cmd, env, market, job, max_spend_usd }) => {
+ try {
+ const key = await solanaKey();
+ if (!key) {
+ return {
+ content: [{ type: "text", text: formatError("No Solana wallet found. Use blockrun_wallet to create one, or set SOLANA_WALLET_KEY.") }],
+ isError: true,
+ };
+ }
+
+ let loaded;
+ try {
+ loaded = await loadSdk(key);
+ } catch {
+ return {
+ content: [{ type: "text", text: formatError("@nosana/sdk is not installed. Run: npm install @nosana/sdk") }],
+ isError: true,
+ };
+ }
+ const { sdk, exposeUrl } = loaded;
+
+ if (action === "status") {
+ if (!job) return { content: [{ type: "text", text: formatError("status needs a job address.") }], isError: true };
+ const result = await sdk.jobs.get(job);
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
+ }
+
+ if (action === "extend") {
+ if (!job) return { content: [{ type: "text", text: formatError("extend needs a job address.") }], isError: true };
+ const plan = planNosanaRental({ seconds: seconds ?? 0, market, maxSpendUsd: max_spend_usd });
+ if (!plan.ok) return { content: [{ type: "text", text: formatError(plan.reason) }], isError: true };
+ const result = await sdk.jobs.extend(job, plan.seconds, false);
+ return {
+ content: [{ type: "text", text: JSON.stringify({ job, added_seconds: plan.seconds, result }, null, 2) }],
+ };
+ }
+
+ const plan = planNosanaRental({ seconds: seconds ?? 0, market, maxSpendUsd: max_spend_usd });
+ if (!plan.ok) return { content: [{ type: "text", text: formatError(plan.reason) }], isError: true };
+ const built = buildNosanaDefinition({ image: image ?? "", port, cmd, env });
+ if ("error" in built) return { content: [{ type: "text", text: formatError(built.error) }], isError: true };
+
+ const ipfsHash = await sdk.ipfs.pin(built.definition);
+ const listed = (await sdk.jobs.list(ipfsHash, plan.seconds, plan.market)) as { job?: string; address?: string } | string;
+ const jobAddress = typeof listed === "string" ? listed : listed.job || listed.address;
+ if (!jobAddress) {
+ return { content: [{ type: "text", text: formatError("The market accepted no job.") }], isError: true };
+ }
+
+ const out = {
+ job: jobAddress,
+ market: plan.market,
+ seconds: plan.seconds,
+ estimated_usd: Number(plan.estimateUsd.toFixed(4)),
+ ipfs: ipfsHash,
+ url: port === undefined ? null : exposeUrl(jobAddress, port),
+ };
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }], structuredContent: out };
+ } catch (err) {
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
+ }
+ }
+ );
+}
diff --git a/test/nosana.test.ts b/test/nosana.test.ts
new file mode 100644
index 0000000..01df32a
--- /dev/null
+++ b/test/nosana.test.ts
@@ -0,0 +1,72 @@
+// Run with: npm test (tsx --test)
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+ planNosanaRental,
+ buildNosanaDefinition,
+ NOSANA_DEFAULT_MARKET,
+ NOSANA_DEFAULT_USD_PER_HOUR,
+} from "../src/tools/nosana.js";
+
+test("planNosanaRental prices by the hour and defaults to the cheapest market", () => {
+ const plan = planNosanaRental({ seconds: 3600 });
+ assert.equal(plan.ok, true);
+ if (!plan.ok) return;
+ assert.equal(plan.market, NOSANA_DEFAULT_MARKET);
+ assert.equal(plan.estimateUsd, NOSANA_DEFAULT_USD_PER_HOUR);
+});
+
+test("planNosanaRental refuses durations the market will not take", () => {
+ for (const seconds of [0, 59, 86401, 1.5, NaN]) {
+ const plan = planNosanaRental({ seconds });
+ assert.equal(plan.ok, false, `${seconds} should be refused`);
+ }
+});
+
+test("planNosanaRental stops a caller from overspending before any key is touched", () => {
+ const plan = planNosanaRental({ seconds: 86400, maxSpendUsd: 0.1 });
+ assert.equal(plan.ok, false);
+ if (plan.ok) return;
+ assert.match(plan.reason, /over the \$0\.1 you allowed/);
+});
+
+test("planNosanaRental allows a lease that fits inside the caller's cap", () => {
+ const plan = planNosanaRental({ seconds: 600, maxSpendUsd: 0.05 });
+ assert.equal(plan.ok, true);
+});
+
+test("buildNosanaDefinition produces a container op the market accepts", () => {
+ const built = buildNosanaDefinition({ image: "docker.io/library/nginx:alpine", port: 8080 });
+ assert.ok("definition" in built);
+ if (!("definition" in built)) return;
+ assert.deepEqual(built.definition, {
+ version: "0.1",
+ type: "container",
+ ops: [{ type: "container/run", id: "rented", args: { image: "docker.io/library/nginx:alpine", gpu: true, expose: 8080 } }],
+ });
+});
+
+test("buildNosanaDefinition rejects an array command instead of letting the node die silently", () => {
+ const built = buildNosanaDefinition({
+ image: "node:20-alpine",
+ cmd: ["sh", "-c", "echo hi"] as unknown as string,
+ });
+ assert.ok("error" in built);
+ if (!("error" in built)) return;
+ assert.match(built.error, /single shell string/);
+});
+
+test("buildNosanaDefinition requires an image", () => {
+ const built = buildNosanaDefinition({ image: "" });
+ assert.ok("error" in built);
+});
+
+test("buildNosanaDefinition omits optional fields rather than sending empty ones", () => {
+ const built = buildNosanaDefinition({ image: "alpine" });
+ assert.ok("definition" in built);
+ if (!("definition" in built)) return;
+ const args = (built.definition as { ops: { args: Record }[] }).ops[0].args;
+ assert.equal("expose" in args, false);
+ assert.equal("cmd" in args, false);
+ assert.equal("env" in args, false);
+});
diff --git a/test/profiles.test.ts b/test/profiles.test.ts
index 883e9a4..7be4f2b 100644
--- a/test/profiles.test.ts
+++ b/test/profiles.test.ts
@@ -4,16 +4,16 @@ import assert from "node:assert/strict";
import { ALL_TOOLS, PROFILES, resolveProfileName, resolveTools } from "../src/profiles.js";
const EXPECTED_COUNTS: Record = {
- full: 19,
+ full: 20,
media: 7,
trading: 8,
research: 6,
chat: 3,
};
-test("ALL_TOOLS has the full 19-tool set", () => {
- assert.equal(ALL_TOOLS.length, 19);
- assert.equal(new Set(ALL_TOOLS).size, 19, "no duplicates");
+test("ALL_TOOLS has the full 20-tool set", () => {
+ assert.equal(ALL_TOOLS.length, 20);
+ assert.equal(new Set(ALL_TOOLS).size, 20, "no duplicates");
});
test("resolveProfileName precedence: --profile flag > env > default", () => {
@@ -46,16 +46,16 @@ test("every profile includes wallet (needed to pay)", () => {
}
});
-test("unknown profile name falls back to full (19 tools)", () => {
+test("unknown profile name falls back to full (20 tools)", () => {
const { profile, tools } = resolveTools(["--profile", "nonsense"], {});
assert.equal(profile, "full");
- assert.equal(tools.size, 19);
+ assert.equal(tools.size, 20);
});
test("no args → full", () => {
const { profile, tools } = resolveTools([], {});
assert.equal(profile, "full");
- assert.equal(tools.size, 19);
+ assert.equal(tools.size, 20);
});
test("Object.prototype key names fall back to full instead of crashing", () => {
@@ -64,7 +64,7 @@ test("Object.prototype key names fall back to full instead of crashing", () => {
for (const name of ["constructor", "__proto__", "toString", "hasOwnProperty"]) {
const { profile, tools } = resolveTools(["--profile", name], {});
assert.equal(profile, "full", `${name} should fall back to full`);
- assert.equal(tools.size, 19, `${name} should expose all 19 tools`);
+ assert.equal(tools.size, 20, `${name} should expose all 19 tools`);
}
});