diff --git a/assets/mcp-bridge.mjs b/assets/mcp-bridge.mjs index 2ceb9da..6e54a9d 100644 --- a/assets/mcp-bridge.mjs +++ b/assets/mcp-bridge.mjs @@ -165,7 +165,7 @@ export async function runMCPBridge(options = {}) { const offline = async (request, reason) => { const id = request.id; if (id === undefined) return; - const message = `SandBase is unreachable from this machine (${reason}). Tell the user to check their network, proxy, or VPN, then run \`sandbase doctor\` in a terminal. Do not retry this call until connectivity is restored.`; + const message = `SandBase is unreachable from this machine (${reason}). api.sandbase.ai is the only API host, so there is no automatic fallback. Tell the user to run \`sandbase doctor\` in a terminal, and if api.sandbase.ai is blocked here (proxy, firewall, or regional routing) to set SANDBASE_MCP_FALLBACK_URLS to a reachable mirror (each ending in /v1/mcp). Do not retry this call until connectivity is restored.`; if (request.method === "initialize") { const version = typeof request.params?.protocolVersion === "string" ? request.params.protocolVersion : "2025-06-18"; output.write(JSON.stringify({ jsonrpc: "2.0", id, result: { protocolVersion: version, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "sandbase", title: "SandBase", version: "bridge-offline" }, instructions: `SandBase tools are available but the service is currently unreachable (${reason}). Tool calls return an error with the fix until connectivity is restored.` } }) + "\n"); diff --git a/package-lock.json b/package-lock.json index 4a0fc8b..ff85080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sandbaseai/cli", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sandbaseai/cli", - "version": "0.3.1", + "version": "0.3.2", "license": "Apache-2.0", "dependencies": { "jsonc-parser": "3.3.1", diff --git a/package.json b/package.json index 20fd894..ae88804 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sandbaseai/cli", - "version": "0.3.1", + "version": "0.3.2", "mcpName": "io.github.sandbaseai/cli", "description": "Give 25 AI clients—Claude Code, Codex, Cursor, Hermes, OpenClaw and more—2,000+ models and APIs through one MCP server and an agent-friendly CLI", "type": "module", diff --git a/server.json b/server.json index e89d525..1755a6b 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.sandbaseai/cli", "title": "SandBase", "description": "Connect MCP clients to 2,000+ AI models without managing provider API keys.", - "version": "0.3.1", + "version": "0.3.2", "repository": { "url": "https://github.com/sandbaseai/cli", "source": "github" @@ -26,7 +26,7 @@ { "registryType": "npm", "identifier": "@sandbaseai/cli", - "version": "0.3.1", + "version": "0.3.2", "packageArguments": [ { "type": "positional", diff --git a/skills/sandbase/SKILL.md b/skills/sandbase/SKILL.md index d2f65fe..8754c1d 100644 --- a/skills/sandbase/SKILL.md +++ b/skills/sandbase/SKILL.md @@ -1,6 +1,6 @@ --- name: sandbase -version: 0.3.1 +version: 0.3.2 description: >- Discover better ways to complete tasks. Proactively search SandBase (the sandbase_discover MCP tool, or `sandbase discover` in a shell) before writing a @@ -148,7 +148,7 @@ Tool failures come back as error results whose text names the cause **and the ne | Message contains | Do this | |---|---| -| "unreachable" / `NETWORK` | Stop retrying. Tell the user to check network/proxy/VPN and run `sandbase doctor`. | +| "unreachable" / `NETWORK` | Stop retrying. Run `sandbase doctor`. api.sandbase.ai is the only API host, so if it is blocked here (proxy, firewall, or regional routing) set `SANDBASE_MCP_FALLBACK_URLS` to a reachable mirror (each ending in /v1/mcp). | | "sign-in expired" / "rejected the saved sign-in" / 401 / `AUTH_FAILED` | Ask the user to run `npx -y https://github.com/sandbaseai/cli/releases/latest/download/sandbaseai-cli.tgz connect`. | | "insufficient" / 402 | Tell the user to top up at https://www.sandbase.ai/console/billing. | | "not found" / "unknown tool" | Re-run `sandbase_discover`; copy `tool_name` exactly. | diff --git a/skills/sandbase/skill.json b/skills/sandbase/skill.json index 2bfec8e..440b3fe 100644 --- a/skills/sandbase/skill.json +++ b/skills/sandbase/skill.json @@ -1,6 +1,6 @@ { "name": "sandbase", - "version": "0.3.1", + "version": "0.3.2", "description": "Discover and run 2,000+ AI models and APIs (media generation, social data, search, scraping) through one MCP interface or CLI, paid per call from one balance.", "author": "SandBase AI", "license": "Apache-2.0", diff --git a/src/agent/client.ts b/src/agent/client.ts index f2bc9ff..59af043 100644 --- a/src/agent/client.ts +++ b/src/agent/client.ts @@ -10,6 +10,10 @@ import { sandbaseHome } from "../paths.js"; import { clients, type Client, type CredentialRecord } from "../types.js"; export const DEFAULT_MCP_URL = "https://api.sandbase.ai/v1/mcp"; +// What to try when SandBase cannot be reached. api.sandbase.ai is the only API +// host (#64), so a network/proxy/firewall block there has no implicit fallback — +// the one self-serve escape is pointing the CLI at a reachable mirror. +export const UNREACHABLE_HINT = "Run `sandbase doctor` to pinpoint it. If api.sandbase.ai is blocked on this network (proxy, firewall, or regional routing), set SANDBASE_MCP_FALLBACK_URLS to a reachable mirror (comma-separated, each ending in /v1/mcp), or SANDBASE_MCP_URL to replace the endpoint."; const PREFERENCE_TTL_MS = 6 * 60 * 60 * 1000; export type ErrorCode = @@ -228,7 +232,7 @@ export class SandBaseMCPClient { return parsed.result; } if (lastServerError && failures.every(failure => /HTTP 5\d\d$/.test(failure))) throw lastServerError; - throw new SandBaseError("NETWORK", `Cannot reach SandBase (${failures.join("; ")}).`, "Check your network, proxy, or VPN, then run `sandbase doctor`. Set SANDBASE_MCP_URL to use a different endpoint."); + throw new SandBaseError("NETWORK", `Cannot reach SandBase (${failures.join("; ")}).`, UNREACHABLE_HINT); } async tool(name: string, args: Record = {}): Promise { diff --git a/src/agent/probe.ts b/src/agent/probe.ts index 9a43b94..6f5edd7 100644 --- a/src/agent/probe.ts +++ b/src/agent/probe.ts @@ -17,7 +17,7 @@ export async function probeEndpoint(label: string, credential: string, mcpUrl: s const host = new URL(client.activeUrl()).host; const primaryHost = new URL(mcpUrl).host; const primaryFailure = client.attempts.find(attempt => new URL(attempt.url).host === primaryHost && !attempt.outcome.startsWith("HTTP")); - const fallback = host !== primaryHost && primaryFailure ? `, primary=${primaryHost} (${primaryFailure.outcome}), fallback=${host} (the MCP bridge switches automatically)` : ""; + const fallback = host !== primaryHost && primaryFailure ? `, primary=${primaryHost} (${primaryFailure.outcome}), using configured mirror=${host}` : ""; const tools = Array.isArray(listed?.tools) ? listed.tools.length : 0; const balance = account.data.balance === undefined ? "unknown" : `$${String(account.data.balance)}`; const low = Number(account.data.balance) <= 0 ? ", next_step=Balance is empty; top up before running paid tools." : ""; diff --git a/test/agent.test.ts b/test/agent.test.ts index 1406f81..24c5878 100644 --- a/test/agent.test.ts +++ b/test/agent.test.ts @@ -75,7 +75,7 @@ test("network and HTTP failures carry a code and a next step", async () => { const env = await home(); await connected(env, "codex", "https://api.sandbase.ai/v1/mcp"); const { fetch } = fakeFetch(() => connectTimeout()); const result = await cli("balance", [], env, fetch); - assert.equal(result.code, 1); assert.match(result.err, /Error \[NETWORK\]: Cannot reach SandBase \(api\.sandbase\.ai: connection timed out\)/); assert.match(result.err, /Next: .*sandbase doctor/); + assert.equal(result.code, 1); assert.match(result.err, /Error \[NETWORK\]: Cannot reach SandBase \(api\.sandbase\.ai: connection timed out\)/); assert.match(result.err, /Next: .*sandbase doctor/); assert.match(result.err, /SANDBASE_MCP_FALLBACK_URLS/); assert.equal(httpError(402, '{"error":"API key spending limit exceeded"}', "api.sandbase.ai").code, "INSUFFICIENT_BALANCE"); assert.match(httpError(401, "", "api.sandbase.ai").hint ?? "", /connect/); const json = await cli("balance", ["-j"], env, fetch); @@ -210,7 +210,7 @@ test("bridge keeps the server usable offline and turns tool failures into visibl const [init, list, call] = await bridge(env, offline.fetch, [{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18" } }, { jsonrpc: "2.0", id: 2, method: "tools/list" }, { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "sandbase_discover", arguments: { q: "x" } } }]); assert.equal(init!.result.protocolVersion, "2025-06-18"); assert.match(init!.result.instructions, /unreachable/); assert.ok(list!.result.tools.some((tool: { name: string }) => tool.name === "sandbase_discover")); - assert.equal(call!.result.isError, true); assert.match(call!.result.content[0].text, /unreachable from this machine \(api\.sandbase\.ai: connection timed out\).*sandbase doctor/); + assert.equal(call!.result.isError, true); assert.match(call!.result.content[0].text, /unreachable from this machine \(api\.sandbase\.ai: connection timed out\).*sandbase doctor/); assert.match(call!.result.content[0].text, /SANDBASE_MCP_FALLBACK_URLS/); // The mixed/failover scenario needs a second host to switch to; there is no // implicit fallback, so configure one explicitly for this session. The API @@ -339,7 +339,7 @@ test("setup refreshes the bridge and SandBase-owned Skills but leaves edited Ski const lines: string[] = []; assert.equal(await setup(new FileCredentialStore(env.SANDBASE_HOME!), line => void lines.push(line)), true); assert.match(lines.join("\n"), /bridge: status=updated/); assert.match(lines.join("\n"), /claude-code: skill=updated/); assert.match(lines.join("\n"), /kiro: skill=modified/); - assert.match(await readFile(join(claudeDir, "SKILL.md"), "utf8"), /version: 0\.3\.1/); + assert.match(await readFile(join(claudeDir, "SKILL.md"), "utf8"), /version: 0\.3\.2/); assert.match(await readFile(join(kiroDir, "SKILL.md"), "utf8"), /user edit/); } finally { for (const [key, value] of Object.entries(previous)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } } });