Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/mcp-bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,7 +26,7 @@
{
"registryType": "npm",
"identifier": "@sandbaseai/cli",
"version": "0.3.1",
"version": "0.3.2",
"packageArguments": [
{
"type": "positional",
Expand Down
4 changes: 2 additions & 2 deletions skills/sandbase/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion skills/sandbase/skill.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/agent/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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<string, unknown> = {}): Promise<ToolOutput> {
Expand Down
2 changes: 1 addition & 1 deletion src/agent/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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." : "";
Expand Down
6 changes: 3 additions & 3 deletions test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<html>", "api.sandbase.ai").hint ?? "", /connect/);
const json = await cli("balance", ["-j"], env, fetch);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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; } }
});
Loading