Skip to content

Latest commit

 

History

History
76 lines (66 loc) · 5.64 KB

AGENTS.md

File metadata and controls

76 lines (66 loc) · 5.64 KB

src/resources — MCP resources + widget registry

↑ src/ · sideways: ../web/AGENTS.md

Three files serving the MCP resources/* surface:

  • resource_service.ts — handles ListResources / ListResourceTemplates / read-resource requests. Takes an optional apifyClient on read, built by the server from the per-request token (_meta.apifyToken || options.token). Token-only by design — no payment headers, so a payment-only session (x402/Skyfire) gets no client and every read fails with InvalidParamsError. Routes every http(s) URI to readApiResource(), which owns the single origin gate; non-http schemes fall through to widgets/usage-guide/fallback. Throws protocol-neutral domain errors (InvalidParamsError/InternalError from src/mcp/errors.ts), never McpError. The protocol adapters map them 1:1 before serialization: legacy_server.ts to v1 McpError, stateless_server.ts to v2 ProtocolError.
  • api_resources.ts — a thin streaming MCP-resource proxy: any Apify API GET endpoint is readable as a resource, identified by its real API URL.
  • widgets.ts — the registry of UI widgets; the widgets themselves are built in ../web.

API resources (api_resources.ts)

Resource URIs are real Apify API GET URLs (https://api.apify.com/v2/...), built from the storage IDs that Actors and tools return. This is a deliberate deviation from the MCP spec's https:// scheme guidance (SHOULD only be used for client-fetchable URLs — ours are token-gated): the identity with the platform's own URLs is the feature. Revisit when tools start emitting resource_links, where clients may fetch https:// URIs directly. isApifyApiUri() gates reads to two origins — the one clients are told to use (getApifyAPIPublicBaseUrl(), ../apify_client.ts) and the egress one (getApifyAPIBaseUrl()), which differ on the hosted server — and rejects userinfo-bearing URLs (axios drops the Authorization header for those, silently degrading to unauthenticated). An accepted URI is sent on the egress origin and returned under the URI as given; templates, instructions, refusals and link-out URLs name only the client-facing origin. readApiResource() then refuses, without a request, what the API tools refuse (validateApiBlocklist in ../tools/api/apify_api_blocklist.ts): the method query parameter and a GET to the synchronous run endpoints. The API tools' callApi (../tools/api/apify_api_request.ts) reuses isApifyApiUri(), isMaxContentLengthAbort(), maskSessionToken(), redactJsonSecrets() and REDACTED, so a change to any changes the tools too. get-key-value-store-record builds its inlined-record URI with buildUrlOnOrigin().

sendApifyApiRequest() (../apify_client.ts, shared with callApi) sends one request through httpClient.axios.request with maxContentLength: MAX_INLINE_BYTES. readApiResource() sends { method: 'GET', responseType: 'stream' } through it, streams the body, and branches on the declared Content-Type: textual base types (text/*, JSON, XML) as text with the full header, decoded with the declared charset (default utf-8; a charset Node cannot decode falls through to blob — lossless beats mangled text, same rule as apify-client's body_parser); everything else (including no Content-Type) as a base64 blob with the base MIME type; empty body as empty text preserving the Content-Type. The body is never parsed, so bytes round-trip exactly, except: the session token becomes [REDACTED] (maskSessionToken(); /v2/browser-info echoes the Authorization header), and so do the urlSigningSecretKey and password values in an application/json body (redactJsonSecrets()). The values are redacted, not removed: removal means re-serializing, which loses formatting and big-number precision and cost about 1 GB on a crafted deep body. axios enforces MAX_INLINE_BYTES (256 KB) mid-consumption on streamed responses (axios ≥1.16: byte-counting wrapper throws ERR_BAD_RESPONSE, counting decoded bytes) — after the request resolves, outside any retry wrapper. On trip, the proxy links out: a text/plain block carrying the store's signed recordPublicUrl for a KVS record, else the token-gated API URL, plus a limit/offset paging hint. sendApifyApiRequest() calls the client's axios instance directly, not httpClient.call(): with a stream body, call() hands non-2xx responses to ApifyApiError unconsumed (junk message, stranded socket per retry) — one attempt, no retries; the error body is read here to surface the API's message.

Genuine failures throw a domain error carrying data: { uri } (SEP-2164 / draft spec: the protocol adapters turn it into a JSON-RPC error, never success-shaped content): 3xx/4xx except 429 → InvalidParamsError; 429, 5xx, no status (network, mid-stream drop) → InternalError. 401/403/429/5xx append a hint via getHttpErrorHint() (shared with tools/call); failures are logged via logHttpError (5xx → exception). Size link-outs are successful reads returning a download pointer, not failures. Discovery is the server-instructions prose plus resources/templates/list, which advertises the common URL shapes (dataset items, KVS record/keys, run, log) as RFC 6570 templates with paging params — advertisement only, not a route table: the read path stays generic and accepts any Apify API GET URL.

Gotcha

widgets.ts is metadata only — it registers and locates widgets. The actual React widget code and design rules live in ../web/AGENTS.md; keep the two in sync.

After any change here run the root Verification steps.