mcpserver
§ · Nested detail page

API marker 客户端 深度解析

拆解 api.ts 如何把 SV Number 的文本 marker、HTTP 状态和 JSON 解析失败转成 Agent 可行动错误。

§ 00 · IMPORT CHAIN

导入链与契约

API client 是整个 server 最薄但最关键的边界:src/api.ts:1 负责其中一个检查点;src/api.ts:3 负责其中一个检查点;src/api.ts:6 负责其中一个检查点;src/api.ts:8 负责其中一个检查点;src/api.ts:10 负责其中一个检查点;src/api.ts:16 负责其中一个检查点;src/api.ts:43 说明外部服务返回 plain text marker,而且仍可能是 HTTP 200。上层工具只 import src/index.ts:15 负责其中一个检查点;src/index.ts:41 负责其中一个检查点;src/index.ts:57 负责其中一个检查点;src/index.ts:122 负责其中一个检查点;src/index.ts:167 负责其中一个检查点;src/index.ts:191,因此所有外部状态都先经过 call、expect 或 json。

src/api.ts · L1-14TypeScript
1// Thin client over the SV Number handler API.
2//
3// The API answers with plain text markers and HTTP 200, so every reply is checked against
4// the marker it is supposed to start with before anything is parsed out of it.
5
6const BASE = process.env.SVN_API_BASE
7  ?? "https://sms-verification-number.com/stubs/handler_api";
8const LANG = process.env.SVN_LANG ?? "en";
9
10export class ApiError extends Error {
11  constructor(public marker: string, message: string) {
12    super(message);
13  }
14}
§ 01 · CORE IMPL

核心实现,逐段拆

▸错误字典

错误表把平台 marker 翻译成下一步动作:src/api.ts:17 负责其中一个检查点;src/api.ts:18 负责其中一个检查点;src/api.ts:21 负责其中一个检查点;src/api.ts:24 负责其中一个检查点;src/api.ts:27 负责其中一个检查点;src/api.ts:29 负责其中一个检查点;src/api.ts:30 负责其中一个检查点;src/api.ts:31 负责其中一个检查点;src/api.ts:32 负责其中一个检查点;src/api.ts:33 负责其中一个检查点;src/api.ts:34 负责其中一个检查点;src/api.ts:35 覆盖 key、参数、余额、库存、activation 和 rate limit。这样 Agent 可以调整 country/operator、充值或放慢轮询,而不是盲目重试。

src/api.ts · L16-36TypeScript
16// what the markers mean, in words an agent can act on
17const ERRORS: Record<string, string> = {
18  BAD_KEY: "The API key is not accepted. Check SVN_API_KEY.",
19  // checked against the live API: an unknown service, an unknown country and an unknown
20  // activation id all come back as ERROR_API, so this is not only a server-side failure
21  ERROR_API: "Either the parameters are wrong (unknown service, country or activation id) "
22    + "or the provider had a hiccup. Check the values against list_services and "
23    + "list_countries before retrying.",
24  ERROR_SQL: "Server-side error on the provider side. Retry with a backoff.",
25  BAD_ACTION: "Malformed parameters, for example an activation id that is not a number.",
26  NO_METHOD: "Unknown action.",
27  BAD_STATUS: "The activation is not in a state where that call makes sense: it may be "
28    + "cancelled, already finished, or still waiting for its first code.",
29  NO_BALANCE: "The account balance is empty. It has to be topped up before ordering.",
30  NO_NUMBERS: "No numbers for this country and service right now. Change country, or set operator to any.",
31  NO_ACTIVATION: "Unknown activation id, or the 20 minutes already ran out.",
32  CANNOT_BEFORE_2_MIN: "This cancel came too early for the API. Wait a couple of minutes and repeat.",
33  REQUEST_LIMIT: "Over the rate limit. Slow the polling down.",
34  WRONG_MAX_PRICE: "The max price is below the current minimum for this service.",
35  BANNED: "The account is blocked.",
36};

▸请求构造不回显密钥

src/api.ts:43 进入 call 后才读取 key,src/api.ts:47 只把它写进 URLSearchParams,src/api.ts:50 直接交给 fetch;页面没有把完整 query 或 key 放回 text response,这对 MCP 工具尤其关键。

▸expect 与 json

call 只负责发请求,expect 和 json 才负责语义校验。src/api.ts:43 负责其中一个检查点;src/api.ts:45 负责其中一个检查点;src/api.ts:47 负责其中一个检查点;src/api.ts:50 负责其中一个检查点;src/api.ts:51 负责其中一个检查点;src/api.ts:52 负责其中一个检查点;src/api.ts:56 负责其中一个检查点;src/api.ts:58 负责其中一个检查点;src/api.ts:59 负责其中一个检查点;src/api.ts:63 负责其中一个检查点;src/api.ts:66 负责其中一个检查点;src/api.ts:68 负责其中一个检查点;src/api.ts:72 显示 marker 不匹配和 JSON parse 失败都会进入 ApiError,这能把 provider 的文本协议收敛成 TypeScript 异常。

src/api.ts · L43-72TypeScript
43export async function call(action: string, params: Record<string, string | number> = {}): Promise<string> {
44  const key = process.env.SVN_API_KEY;
45  if (!key) throw new Error("SVN_API_KEY is not set in the environment.");
46
47  const query = new URLSearchParams({ api_key: key, action, lang: LANG });
48  for (const [name, value] of Object.entries(params)) query.set(name, String(value));
49
50  const response = await fetch(`${BASE}?${query}`, { headers: { Accept: "*/*" } });
51  if (!response.ok) throw new Error(`HTTP ${response.status} from the API.`);
52  return (await response.text()).trim();
53}
54
55/** Calls an action that must answer with `marker`, and returns the reply. */
56export async function expect(marker: string, action: string,
57                             params: Record<string, string | number> = {}): Promise<string> {
58  const reply = await call(action, params);
59  if (!reply.startsWith(marker)) throw new ApiError(reply.split(":")[0], explain(reply));
60  return reply;
61}
62
63export async function json<T>(action: string, params: Record<string, string | number> = {}): Promise<T> {
64  const reply = await call(action, params);
65  try {
66    return JSON.parse(reply) as T;
67  } catch {
68    throw new ApiError(reply.split(":")[0], explain(reply));
69  }
70}
71
72export const pollSeconds = Number(process.env.SVN_POLL_SECONDS ?? 4);
§ 02 · KEY FLOW

关键流程

tool handler → call(action, params) → fetch(BASE?query) → expect(marker) / JSON.parse → ApiError(explain)

这条链路的设计重点是“晚绑定”:src/api.ts:47 负责其中一个检查点;src/api.ts:48 负责其中一个检查点;src/api.ts:50 到请求前才拼参数,src/api.ts:58 负责其中一个检查点;src/api.ts:59 到具体工具才校验期望 marker,src/index.ts:167 负责其中一个检查点;src/index.ts:215 负责其中一个检查点;src/index.ts:231 负责其中一个检查点;src/index.ts:246 让每个生命周期工具声明自己期待的成功前缀。

§ 03 · EDGE CASES

边界 / 失败模式

  • src/api.ts:45 缺 key 直接阻断,避免向 provider 发送无效请求。
  • src/api.ts:51 非 2xx HTTP 状态仍会变成普通 Error。
  • src/api.ts:68 JSON 解析失败时仍会提取 marker 解释,而不是丢给 Agent 一段不可读文本。
Sub-page · 2026-08-12Source · sv-number/mcp-server↑ mcp-server 主页面