분석된 URL
https://mdapi.io
측정: 22시간 전
AI-Ready 점수
미흡
/ 100
토큰 절감량
점수 상세
신흥 프로토콜
6개 중 4개 감지AI 에이전트가 찾는 well-known 엔드포인트. 감지되면 에이전트가 서비스를 자동으로 발견하고 연결할 수 있습니다.
-
OAuth Protected Resource RFC 9728
/.well-known/oauth-protected-resource -
OAuth Discovery RFC 8414
/.well-known/oauth-authorization-server -
MCP Server Card SEP-1649 draft
/.well-known/mcp/server-card.json- v1.0.0
- 1 tool(s)
- 15 resource(s)
-
A2A Agent Card A2A v1.0
/.well-known/agent-card.json- name: mdapi
- v1.0.0
- 3 capabilities
- 7 skill(s)
-
API Catalog RFC 9727
/.well-known/api-catalog- 1 API(s)
-
Agent Skills index Discovery RFC v0.2.0 draft
/.well-known/agent-skills/index.json- 1 skill(s)
- mdapi-conversion
이 점수는 그대로 있지 않습니다. 모니터링은 아직 없습니다. 명단에 등록하면 출시할 때 알려드립니다.
명단에 등록되었습니다! 서비스 출시 시 알려드리겠습니다.
측정 결과 5/5 AI search bots blocked: OAI-SearchBot, Claude-SearchBot, PerplexityBot, DuckAssistBot, Amazonbot
robots.txt가 일부 AI 봇(GPTBot, ClaudeBot, PerplexityBot)을 차단하고 있습니다. 이로 인해 AI 에이전트가 콘텐츠를 색인할 수 없습니다.
구현 방법
robots.txt를 업데이트하여 AI 봇을 허용하세요. GPTBot, ClaudeBot, PerplexityBot에 대한 명시적 Allow 규칙을 추가하세요.
측정 결과 Main content starts at 100% of HTML
주요 콘텐츠가 HTML 문서의 후반부에 나타납니다. AI 에이전트는 일찍 나타나는 콘텐츠에 더 높은 가중치를 줄 수 있습니다.
구현 방법
<main> 또는 <article> 콘텐츠가 사이드바와 부가 콘텐츠보다 먼저 나타나도록 HTML을 재구성하세요.
측정 결과 No JSON-LD / Schema.org found
Schema.org 구조화 데이터를 찾을 수 없습니다. JSON-LD는 AI 에이전트가 페이지에서 사실 기반의 구조화 정보를 추출하는 데 도움을 줍니다.
구현 방법
Schema.org 마크업이 포함된 <script type="application/ld+json"> 블록을 추가하세요. 적절한 유형을 사용하세요: 블로그 게시물에는 Article, 제품 페이지에는 Product, 회사 페이지에는 Organization.
측정 결과 No headings found
제목 구조에 문제가 있습니다 (레벨 건너뛰기 또는 다중 h1 태그). 깔끔한 계층 구조는 AI 에이전트가 콘텐츠 구성을 이해하는 데 도움을 줍니다.
구현 방법
페이지당 정확히 하나의 <h1>을 유지하고, 제목이 순차적 순서를 따르도록 하세요: h1 > h2 > h3. 레벨을 건너뛰지 마세요 (예: h1에서 바로 h3).
측정 결과 0/3 OG tags present
Open Graph 태그가 없거나 불완전합니다. OG 태그는 AI 에이전트(및 소셜 플랫폼)가 페이지의 제목, 설명, 이미지를 이해하는 데 도움을 줍니다.
구현 방법
페이지의 <head>에 og:title, og:description, og:image 메타 태그를 추가하세요.
측정 결과 No meta description
메타 설명을 찾을 수 없습니다. 이 태그는 AI 에이전트와 검색 엔진에 페이지의 간결한 요약을 제공합니다.
구현 방법
페이지 콘텐츠에 대한 150-160자 설명이 포함된 <meta name="description" content="..."> 태그를 추가하세요.
측정 결과 0% token reduction (HTML→Markdown)
HTML-Markdown 변환의 토큰 감소율이 낮습니다. 콘텐츠 대비 마크업이 과도합니다.
구현 방법
불필요한 마크업, 인라인 스타일, 표현 요소를 제거하세요. Markdown으로 깔끔하게 변환되는 콘텐츠 중심 HTML을 유지하세요.
측정 결과 Missing <article> and <main> elements
페이지에 <article> 또는 <main> 요소가 없습니다. 이 시맨틱 컨테이너는 AI 에이전트가 주요 콘텐츠 영역을 식별하고 내비게이션, 사이드바, 푸터를 무시하는 데 도움을 줍니다.
구현 방법
페이지의 주요 콘텐츠 주위에 <main> 요소를 추가하고, 블로그 게시물이나 제품 설명 같은 독립적인 콘텐츠 블록에는 <article>을 사용하세요.
측정 결과 CDN level (Cloudflare) — Content negotiation
이 사이트는 Markdown for Agents를 일부만 지원합니다. 이미 갖춘 방식은 위의 «측정한 내용»에 나열되어 있습니다. 나머지를 추가하면 더 많은 에이전트가 HTML을 파싱하는 대신 markdown을 요청할 수 있어 토큰 사용이 약 80% 줄어듭니다.
구현 방법
- /page.md처럼 두 번째 URL로 Markdown 쌍둥이를 제공하세요.
- HTML head에 <link rel="alternate" type="text/markdown" href="…">를 추가하세요.
- Markdown 버전을 가리키는 Link HTTP 헤더를 추가하세요: Link: <…>; rel="alternate"; type="text/markdown".
측정 결과 No canonical URL
정규 URL을 찾을 수 없습니다. AI 에이전트가 페이지의 선호 버전을 식별하고 중복 콘텐츠를 방지하는 데 도움을 줍니다.
구현 방법
페이지의 정규 URL을 가리키는 <link rel="canonical" href="..."> 태그를 추가하세요.
\# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems. Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage. ## Agent entrypoint - \*\*Start AI discovery\*\* → https://mdapi.io/.well-known/ai-discovery.json - \*\*Use skill\*\* → https://mdapi.io/.well-known/skill.md ## Quick Start Choose your entry point based on your role: | Role | Protocol | Endpoint | When to use | | ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). | | AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. | | AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. | | OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. | | Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. | ### Universal discovery All protocols and capabilities are described in one file: GET /.well-known/ai-discovery.json ## Features - Stateless, in-memory processing - Edge execution with automatic scaling - Prompt-driven transformation - AI-optimized output for LLMs - Pay-per-use via x402 v1/v2 or manual payment ## Supported Formats | Type | Formats | | --------- | ------------------------------ | | Documents | PDF, DOCX, XLSX, XLS, ODT, ODS | | Images | JPEG, JPG, PNG, WebP, SVG | | Text | HTML, XML, JSON, CSV, TXT | | Webpages | Any publicly accessible URL | ## Source Parameters (all protocols) Every protocol (REST, MCP, ACP, A2A, OpenAI) converges on the \*\*same conversion core\*\*, so content is specified via a single unified \`input\` parameter everywhere. | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | \`input\` | string | Content to convert: a URL (http(s)://...), a data URI (data:mime/type;base64,...), or plain text. Auto-detected: http(s):// → URL, data: → file, otherwise → text. | The \`input\` parameter is auto-detected by the core: URLs (starting with \`http://\` or \`https://\`) are fetched, data URIs (starting with \`data:\`) are decoded as files, and anything else is treated as raw text. Additional parameters (\`prompt\`, \`result\`, \`stream\`, \`token\`, \`memo\`) are orthogonal and may be combined with \`input\`. All five protocols expose the same \`input\` source and apply the same transformations, streaming, and prompt-driven processing - the only difference is the transport (REST query/JSON, MCP \`tools/call\`, ACP \`session/prompt\`, OpenAI \`messages\`, A2A \`message.parts\`). ## Limits | Limit | Value | | ------------------- | -------------------------------------------------------------------------------- | | \*\*Max file size\*\* | 50 MB | | \*\*Max URL content\*\* | 50 MB | | \*\*Rate limit\*\* | 10,000 requests per hour | | \*\*Free tier\*\* | 10 requests per day (no token required), within the service’s overall free quota | | \*\*Paid tier\*\* | min $0.01 per conversion (USDC on Solana) | | \*\*Token validity\*\* | 1 year | ## Authentication \*\*Recommended:\*\* Use \`Authorization: Bearer TOKEN\` | Method | Use Case | | -------------------- | ---------------------------------- | | Bearer (recommended) | \`-H "Authorization: Bearer TOKEN"\` | | Header | \`-H "X-Token-Required: TOKEN"\` | ## Token Activation Before you can use a paid token, you must receive a 402 response first: | Step | Description | | ---- | ------------------------------------------------------- | | 1 | Request without token → Receive 402 with NEW token+memo | | 2 | Send USDC on Solana to wallet with memo from 402 | | 3 | Retry with EXACT token+memo from 402 → Activation | | 4 | After: use token only (no memo needed) | Important: The token+memo issued in the 402 response MUST be used exactly. Using old token or different memo will be rejected. ## API Usage ### GET / (Content conversion) Simple content conversion using query parameters. Returns Markdown directly. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \`input\` | string | \* | Content to convert (URL, text, or data URI - auto-detected) | | \`prompt\` | string | | Custom instructions for LLM processing | | \`result\` | string | | Response format: \`markdown\`, \`prompt\`, or \`both\` | | \`stream\` | boolean | | Enable streaming: true for SSE response | | \`token\` | string | | Access token for paid tier | | \`memo\` | string | | Memo for token activation | \*The \`input\` parameter is required.\* > \*\*⚠️ Browser URL limit:\*\* GET requests with long \`input\` or \`prompt\` values may exceed browser URL limits (~2048 characters). Use POST with JSON body for large payloads. ### POST / (Content conversion via JSON) Supports content conversion via JSON body. The \`input\` parameter accepts URLs, text, or data URIs (auto-detected). Returns a JSON object containing the Markdown content. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \`input\` | string | \* | Content to convert (URL, text, or data URI - auto-detected) | | \`prompt\` | string | | Custom instructions for LLM processing | | \`result\` | string | | Response format: \`markdown\`, \`prompt\`, or \`both\` | | \`stream\` | boolean | | Enable streaming: true for SSE response | | \`token\` | string | | Access token for paid tier | | \`memo\` | string | | Memo for token activation | \*The \`input\` parameter is required.\* ### Result Format Parameter The \`result\` parameter controls the response format for both GET and POST requests. | Value | Description | | -------------------- | --------------------------------------------------------------- | | \`markdown\` (default) | Returns the converted Markdown content | | \`prompt\` | Returns the result of LLM processing with \`prompt\` instructions | | \`both\` | Returns both \`markdown\` and \`prompt\_result\` in the response | When \`result=both\`: - \*\*GET requests\*\* return Markdown combining \`markdown\`, followed by "## Prompt Result" and \`prompt\_result\` (always in Markdown format) - \*\*POST requests\*\* return JSON with \`markdown\` and \`prompt\_result\` fields > \*\*Auto \`result\`:\*\* When \`prompt\` is provided without an explicit \`result\`, the core automatically sets \`result="prompt"\` (LLM output only). Without \`prompt\`, default is \`result="markdown"\`. Only specify \`result\` explicitly when you need both (\`result="both"\`). ### Prompt Parameter The \`prompt\` parameter lets you specify custom instructions for the LLM to follow when generating the result. | Use Case | Example | | ------------------ | ---------------------------------------------------------- | | Summarize | \`?input=https://example.com&prompt=Summarize\` | | Extract key points | \`?input=Hello World&prompt=Extract key points\` | | Convert to JSON | \`?input=https://example.com&prompt=Convert to JSON format\` | | Analyze content | \`?input=Hello World&prompt=Analyze and explain\` | ### Streaming Parameter The \`stream\` parameter enables Server-Sent Events (SSE) streaming for real-time response delivery. \*\*Type:\*\* \`boolean\` \*\*Default:\*\* \`false\` (non-streaming) Example: \`\`\`bash curl "https://mdapi.io/?input=...&stream=true" \`\`\` Response format (OpenAI-compatible SSE, one JSON object per \`data:\` line): \`\`\`json data: {"type":"token\_info","token\_status":"valid","token\_balance":0.99,"token\_expires":1798761600} data: {"choices":\[{"index":0,"delta":{"content":" chunk"},"finish\_reason":null}\]} data: {"choices":\[{"index":0,"delta":{"content":" more"},"finish\_reason":null}\]} data: {"choices":\[{"index":0,"delta":{},"finish\_reason":"stop"}\]} data: \[DONE\] \`\`\` \*\*Native streaming per protocol.\*\* Every protocol delivers a \*real\* content stream when \`stream: true\`, but each emits it in its own native frame format (so existing clients keep working): | Protocol | Streaming frame format | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | REST | OpenAI-compatible \`choices/delta\` frames | | OpenAI | \`chat.completion.chunk\` (\`choices/delta\`) | | MCP | \`notifications/message\` content chunks, then one final \`tools/call\` result frame | | ACP | \`session/update\` notification chunks (one stable \`messageId\` per turn), then a final response carrying only \`stopReason\` | | A2A | \`result.task\` (\`TASK\_STATE\_WORKING\`) start frame, \`result.artifactUpdate\` (\`{artifact, append, lastChunk}\`) content frames, then \`result.statusUpdate\` (\`TASK\_STATE\_COMPLETED\`) - stream closes | > \*\*Note on MCP transport vs. the \`stream\` parameter.\*\* The MCP manifest advertises \`transport.type: "streamable-http"\` - that is the MCP \*transport\* > (how JSON-RPC requests are delivered to \`POST /mcp\`). It is unrelated to the \`stream\` \*parameter\*, which independently enables SSE streaming of the > conversion \*\*content\*\*. You can use MCP without streaming; and when you do pass \`stream: true\`, the content arrives as SSE frames alongside the transport. ### Response Codes | Code | Description | Response Body (GET) | Response Body (POST) | | ---- | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | | 200 | Success | Markdown content | JSON with \`success\`, markdown, prompt\_result, metrics, token fields | | 402 | Payment Required | Markdown payment instructions (\`X-Error-Code: payment\_required\`) | JSON with \`success:false\`, \`code:"payment\_required"\`, and payment object | | 400 | Bad Request | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"invalid\_request"}\` | | 401 | Invalid Token | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"unauthorized"}\` | | 404 | Not Found | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"not\_found"}\` | | 413 | Payload Too Large | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"too\_large"}\` | | 429 | Rate Limited | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"rate\_limited"}\` | | 500 | Server Error | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"server\_error"}\` | ### Token Status The X-Token-Status header (and token\_status field in responses) indicates the current state of authentication: | Status | Description | | ------------------ | --------------------------------------------------------------------------------------- | | free | Free tier (no token required, 10 requests/day), within the service's overall free quota | | valid | Paid token active with remaining balance | | invalid | Token not found or not provided | | expired | Token validity period has ended | | exhausted | Token balance has been fully used | | expired\_pending | Activation memo has expired | | activated | Token was just activated with this request | | verification\_error | Payment verification failed | | invalid\_payment | Payment transaction is invalid | | error | Internal error during token processing | | pending | Payment required (token not yet activated) | ### Endpoints | Method | Path | Description | | ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | GET | /about | About service | | GET | / | API docs or conversion (via query parameter: \`input\`, \`prompt\`, \`result\`) | | POST | / | Convert content via JSON body (supports \`input\`, \`prompt\`, \`result\` params) | | POST | /v1/chat/completions | OpenAI-compatible endpoint | | GET | /mcp | MCP server manifest | | POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) | | POST | /acp | ACP RPC endpoint (IDE agents) | | POST | /a2a | A2A RPC endpoint (agent2agent) | | GET | /health | Health check | | GET | /llms.txt | API documentation | | GET | /llms-full.txt | Full API documentation | | GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery | | GET | /.well-known/agent.json or /agent.json | AI Agent discovery | | GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card | | GET | /.well-known/acp.json or /acp.json | ACP manifest | | GET | /.well-known/x402.json or /x402.json | x402 payment manifest | | GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) | | GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) | | GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) | | GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) | | GET | /.well-known/skills/index.json | Legacy skills index | | GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) | | GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) | | GET | /.well-known/mcp/server-cards.json | MCP server cards index | | GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest | | GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config | | GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill | #### Examples \`\`\`bash # URL conversion via GET (free) curl "https://mdapi.io/?input=https://example.com" # URL with prompt and result=both (returns markdown + prompt\_result) curl "https://mdapi.io/?input=https://example.com&prompt=Summarize&result=both" # Text with prompt (auto result=prompt) curl "https://mdapi.io/?input=Hello World&prompt=Summarize" # Token activation via GET (activate and use) curl -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" "https://mdapi.io/?input=https://example.com" # Paid request with token via GET (using token) curl -H "Authorization: Bearer YOUR\_TOKEN" "https://mdapi.io/?input=https://example.com" # URL conversion via POST (free) curl -X POST -H "Content-Type: application/json" -d '{"input":"https://example.com"}' "https://mdapi.io/" # Text with prompt via POST curl -X POST -H "Content-Type: application/json" -d '{"input":"Hello World","prompt":"Summarize","result":"both"}' "https://mdapi.io/" # File upload via POST (data URI) curl -X POST -H "Content-Type: application/json" -d '{"input":"data:text/plain;base64,SGVsbG8gV29ybGQ="}' "https://mdapi.io/" # Token activation via POST curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" -d '{"input":"https://example.com"}' "https://mdapi.io/" # Paid request with token via POST curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer YOUR\_TOKEN" -d '{"input":"https://example.com"}' "https://mdapi.io/" \`\`\` ### OpenAI Compatible Endpoint The \`/v1/chat/completions\` endpoint provides an OpenAI‑compatible API for markdown conversion with streaming support. \*\*Supported features:\*\* - URL extraction from message content (any text containing https?://) - image\_url in messages (OpenAI format) - supports HTTP URLs and data URLs - file in messages (OpenAI format) - base64 encoded files (field \`file.data\`, optional \`file.mimeType\`; built into a \`data:\` URI for the core - no \`file.filename\` required or used) - Direct text content in messages (any text without a URL is sent to the core as the \`input\` source and converted to Markdown) - Token and memo via headers (recommended for POST) - Streaming SSE responses (\`stream: true\`) - Custom instructions with LLM processing (system messages, or user messages containing instruction keywords such as \*extract, summarize, analyze, format, convert to, write as, create, generate, json\* → LLM-driven summary/extraction/transformation) - \`prompt\` for LLM-processed output. The response surfaces \`prompt\_result\` at the top level alongside the standard \`choices\[\].message.content\` (which carries \`prompt\_result\` when prompt is set, otherwise the Markdown). \`model\` is accepted but not required (any string; the service uses its own conversion pipeline, not a remote LLM chat model, unless custom instructions trigger LLM processing). > \*\*Content via message text:\*\* the message text is passed to the core as the \`input\` parameter - the same unified source as every other protocol. URLs are auto-detected, data URIs are decoded as files, and plain text is processed directly. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). #### Request Schema \`\`\`json { "type": "object", "properties": { "model": { "type": "string", "description": "Optional model identifier (any string accepted; not required)" }, "messages": { "type": "array", "items": { "type": "object", "properties": { "role": {"type": "string", "enum": \["user", "system", "assistant"\]}, "content": {"oneOf": \[{"type": "string"}, {"type": "array"}\]} } }, "description": "Chat messages. URL in content, image\_url or file in content for input" }, "stream": { "type": "boolean", "default": false, "description": "Enable streaming SSE responses" }, "prompt": { "type": "string", "description": "Custom instructions for LLM processing (alternative to instruction keywords in messages)" }, "result": { "type": "string", "enum": \["markdown", "prompt", "both"\], "description": "Response format when using prompt" }, "token": {"type": "string", "description": "Access token for paid tier"}, "memo": {"type": "string", "description": "Memo for token activation"}, "input": { "type": "string", "description": "Content to convert (URL, text, or data URI - auto-detected). Alternative to a URL/file embedded in messages" } }, "required": \["messages"\] } \`\`\` > \*\*Note on streaming + custom instructions:\*\* when custom instructions trigger LLM processing, the response is returned as a single completion (streaming is not applied to the LLM pass). Streaming SSE applies to the standard conversion path. ## MCP Configuration Connect mdapi.io to your MCP-compatible client (spec 2026-07-28, stateless). > \*\*Single source via \`input\`:\*\* the \`convert\` tool accepts a unified \`input\` parameter - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). ### Protocol Requirements - \*\*Transport:\*\* Streamable HTTP (POST-only for JSON-RPC, GET for manifest) - \*\*Required headers:\*\* \`MCP-Protocol-Version: 2026-07-28\` and \`Mcp-Method\` on every request; \`Mcp-Name\` additionally on \`tools/call\`, \`resources/read\`, and \`prompts/get\` - \*\*Stateless:\*\* No sessions - every request is independent - \*\*Discovery:\*\* Use \`server/discover\` to query server capabilities and supported versions ### Basic Configuration Add to your MCP config file: \`\`\`json { "mcpServers": { "mdapi": { "url": "https://mdapi.io/mcp" } } } \`\`\` > \*\*Note:\*\* No token is required to connect. A free tier is available (10 requests per day), within the service’s overall free quota. ### OpenClaw Integration OpenClaw can use mdapi.io in two ways: \*\*Option 1: Via MCP (Recommended)\*\* \`\`\`json { "mcpServers": { "mdapi": { "url": "https://mdapi.io/mcp" } } } \`\`\` \*\*Option 2: Via OpenAI-compatible endpoint\*\* \`\`\`bash openclaw config set llm.apiBase https://mdapi.io/v1 openclaw config set llm.apiKey YOUR\_TOKEN \`\`\` ### Using MCP with a token MCP does not use HTTP-level Authorization headers. The token is always passed inside the tool \`arguments\` object. \*\*Activation\*\* - include \`token\` + \`memo\` in the first request: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "token": "YOUR\_TOKEN", "memo": "YOUR\_PAYMENT\_MEMO" } } } \`\`\` \*\*After activation\*\* - use \`token\` only (no memo needed): \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "token": "YOUR\_ACTIVATED\_TOKEN" } } } \`\`\` ### MCP Tool Examples Convert with prompt and result: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "prompt": "Summarize", "result": "both", "token": "YOUR\_TOKEN" } } } \`\`\` Process text directly: \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "Hello World", "prompt": "Extract key points", "result": "prompt" } } } \`\`\` Stream with SSE (native MCP frames): \`\`\`json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "stream": true } } } \`\`\` Response (SSE over \`streamable-http\`): intermediate \`notifications/message\` content chunks, then one final \`tools/call\` result frame with the full Markdown: \`\`\` data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":" partial "}} data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":" more "}} data: {"jsonrpc":"2.0","id":3,"result":{"content":\[{"type":"text","text":""}\],"isError":false}} data: \[DONE\] \`\`\` ### Using Environment Variables Or use environment variable: \`\`\`bash export MDAPI\_TOKEN=YOUR\_ACTIVATED\_TOKEN \`\`\` ## Code Examples ### JavaScript (fetch) \`\`\`javascript // Convert a URL via GET - returns Markdown directly const response = await fetch('https://mdapi.io/?input=https://example.com'); const markdown = await response.text(); console.log(markdown); \`\`\` \`\`\`javascript // Convert a URL via POST - returns JSON with metadata const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); console.log(data.markdown); \`\`\` \`\`\`javascript // Text with prompt - returns prompt\_result const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'Hello World', prompt: 'Summarize', result: 'both' }) }); const data = await response.json(); console.log(data.markdown); console.log(data.prompt\_result); \`\`\` \`\`\`javascript // File upload via data URI const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'data:text/plain;base64,SGVsbG8gV29ybGQ=' }) }); const data = await response.json(); console.log(data.markdown); \`\`\` \`\`\`javascript // Token activation - first request with token + memo const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\_TOKEN', 'X-Memo-Required': 'YOUR\_MEMO' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); // After activation, use token only (no memo needed) \`\`\` \`\`\`javascript // Streaming via OpenAI-compatible endpoint const response = await fetch('https://mdapi.io/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\_TOKEN' }, body: JSON.stringify({ model: 'markdown-v1', messages: \[{ role: 'user', content: 'Convert https://example.com' }\], stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; process.stdout.write(decoder.decode(value)); } \`\`\` ### Python \`\`\`python import requests # Convert a URL via GET - returns Markdown directly response = requests.get('https://mdapi.io/?input=https://example.com') response.raise\_for\_status() print(response.text) \`\`\` \`\`\`python import requests # Convert a URL via POST - returns JSON with metadata response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) \`\`\` \`\`\`python import requests # Text with prompt - returns prompt\_result response = requests.post( 'https://mdapi.io/', json={'input': 'Hello World', 'prompt': 'Summarize', 'result': 'both'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) print(data\['prompt\_result'\]) \`\`\` \`\`\`python import requests # File upload via data URI with open('document.pdf', 'rb') as f: import base64 file\_data = base64.b64encode(f.read()).decode() response = requests.post( 'https://mdapi.io/', json={'input': f'data:text/plain;base64,{file\_data}'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) \`\`\` \`\`\`python import requests # Token activation response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'}, headers={ 'Authorization': 'Bearer YOUR\_TOKEN', 'X-Memo-Required': 'YOUR\_MEMO' } ) response.raise\_for\_status() # After activation, use token only (no memo needed) \`\`\` \`\`\`python from openai import OpenAI # OpenAI-compatible streaming client = OpenAI(base\_url='https://mdapi.io/v1', api\_key='YOUR\_TOKEN') stream = client.chat.completions.create( model='markdown-v1', messages=\[{'role': 'user', 'content': 'Convert https://example.com'}\], stream=True ) for chunk in stream: if chunk.choices\[0\].delta.content: print(chunk.choices\[0\].delta.content, end='') \`\`\` ### Go \`\`\`go package main import ( "fmt" "io" "net/http" ) func main() { // Convert a URL via GET - returns Markdown directly resp, err := http.Get("https://mdapi.io/?input=https://example.com") if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() body, \_ := io.ReadAll(resp.Body) fmt.Println(string(body)) } \`\`\` \`\`\`go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { // Convert a URL via POST - returns JSON with metadata payload, \_ := json.Marshal(map\[string\]string{ "input": "https://example.com", }) resp, err := http.Post( "https://mdapi.io/", "application/json", bytes.NewReader(payload), ) if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() var result map\[string\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\["markdown"\]) } \`\`\` \`\`\`go package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "net/http" "os" ) func main() { // File upload via data URI fileBytes, \_ := os.ReadFile("document.pdf") b64 := base64.StdEncoding.EncodeToString(fileBytes) payload, \_ := json.Marshal(map\[string\]string{ "input": "data:text/plain;base64," + b64, }) resp, err := http.Post( "https://mdapi.io/", "application/json", bytes.NewReader(payload), ) if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() var result map\[string\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\["markdown"\]) } \`\`\` ### Rust \`\`\`rust use anyhow::Result; use reqwest::Client; #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let url = "https://mdapi.io/?input=https://example.com"; let response = client.get(url).send().await?; response.error\_for\_status\_ref()?; let markdown = response.text().await?; println!("{}", markdown); Ok(()) } \`\`\` \`\`\`rust use anyhow::{Result, Context}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #\[derive(Deserialize)\] struct ApiResponse { markdown: Option, prompt\_result: Option, } #\[derive(Serialize)\] struct ConvertRequest { input: String, #\[serde(skip\_serializing\_if = "Option::is\_none")\] prompt: Option, #\[serde(skip\_serializing\_if = "Option::is\_none")\] result: Option, } pub struct MdApiClient { client: Client, base\_url: String, token: Option, } impl MdApiClient { pub fn new(token: Option) -> Self { Self { client: Client::new(), base\_url: "https://mdapi.io".to\_string(), token, } } pub async fn convert\_url( &self, url: &str, prompt: Option<&str>, ) -> Result { let body = ConvertRequest { input: url.to\_string(), prompt: prompt.map(|p| p.to\_string()), result: prompt.map(|\_| "both".to\_string()), }; let mut request = self.client .post(&self.base\_url) .header("Content-Type", "application/json") .json(&body); if let Some(ref token) = self.token { request = request.header("Authorization", format!("Bearer {}", token)); } let response = request .send() .await .context("Failed to send HTTP request")?; response .error\_for\_status\_ref() .context("API returned error status")?; response .text() .await .context("Failed to read response body") } } // Example: convert a URL with prompt #\[tokio::main\] async fn main() -> Result<()> { let token = std::env::var("MDAPI\_TOKEN").ok(); let client = MdApiClient::new(token); let markdown = client .convert\_url("https://example.com", Some("Summarize")) .await?; println!("{}", markdown); Ok(()) } \`\`\` \`\`\`rust use anyhow::Result; use reqwest::Client; use base64::engine::general\_purpose::STANDARD; use base64::Engine; #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let file\_bytes = std::fs::read("document.pdf")?; let b64 = STANDARD.encode(&file\_bytes); let body = serde\_json::json!({ "input": format!("data:text/plain;base64,{}", b64) }); let response = client .post("https://mdapi.io/") .header("Content-Type", "application/json") .json(&body) .send() .await?; let data: serde\_json::Value = response.json().await?; println!("{}", data\["markdown"\]); Ok(()) } \`\`\` \`\`\`rust use anyhow::Result; use futures\_util::stream::StreamExt; use reqwest::Client; // Streaming response when using the OpenAI‑compatible endpoint with stream = true #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let token = std::env::var("MDAPI\_TOKEN")?; let body = serde\_json::json!({ "model": "markdown-v1", "messages": \[ { "role": "user", "content": "Convert https://example.com" } \], "stream": true }); let response = client .post("https://mdapi.io/v1/chat/completions") .header("Authorization", format!("Bearer {}", token)) .json(&body) .send() .await?; response.error\_for\_status\_ref()?; let mut stream = response.bytes\_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; let text = String::from\_utf8\_lossy(&chunk); eprint!("{}", text); } Ok(()) } \`\`\` ### OpenAI SDK \`\`\`python from openai import OpenAI client = OpenAI( base\_url="https://mdapi.io/v1", api\_key="YOUR\_TOKEN" ) try: response = client.chat.completions.create( model="markdown-v1", messages=\[{"role": "user", "content": "Convert https://example.com"}\] ) print(response.choices\[0\].message.content) except Exception as e: print("API error:", e) \`\`\` #### OpenAI with paid token \`\`\`bash curl -X POST "https://mdapi.io/v1/chat/completions" -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" -H "Content-Type: application/json" -d '{"model":"markdown-v1","messages":\[{"role":"user","content":"Convert https://example.com"}\]}' \`\`\` After activation, use token only (no memo needed): \`\`\`bash curl -X POST "https://mdapi.io/v1/chat/completions" -H "Authorization: Bearer YOUR\_ACTIVATED\_TOKEN" -H "Content-Type: application/json" -d '{"model":"markdown-v1","messages":\[{"role":"user","content":"Convert https://example.com"}\]}' \`\`\` ## A2A Configuration Connect mdapi.io to your A2A-compatible agent (Claude Code, Codex, OpenClaw, Hermes, etc.). ### Basic Configuration Add to your A2A client configuration: \`\`\`json { "agent": { "name": "mdapi", "agentCard": { "url": "https://mdapi.io/.well-known/agent-card.json" } } } \`\`\` Or use JSON-RPC directly: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-1", "parts": \[ { "text": "Convert https://example.com" } \] } } } \`\`\` ### A2A Methods | Method | Description | | -------------------- | --------------------------------------- | | SendMessage | Send a message to initiate conversion | | SendStreamingMessage | Send message with SSE streaming updates | | GetTask | Get task status and results by ID | | ListTasks | List tasks with optional filtering | | CancelTask | Cancel an in-progress task | | SubscribeToTask | Subscribe to task updates via SSE | > \*\*Single source via \`input\`:\*\* the \`input\` parameter in the message parts is the unified source - the same as the REST endpoint. A bare URL > inside a text part (e.g. \`"Convert https://example.com"\`) is extracted automatically and used as the conversion source, so you don't need to wrap it > in structured JSON. Instructions such as \`Summarize\` should be passed via the structured \`{ "input": "...", "prompt": "..." }\` form, not mixed into the text. ### A2A Examples #### SendMessage \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-1", "parts": \[ { "text": "Convert https://example.com" } \] } } }' \`\`\` #### SendMessage with file (data URI) \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 2, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-2", "parts": \[ { "text": "{"input":"data:text/plain;base64,SGVsbG8gV29ybGQ="}" } \] } } }' \`\`\` #### SendMessage with structured data \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 3, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-3", "parts": \[ { "data": { "input": "https://example.com", "result": "markdown" }, "mediaType": "application/json" } \] } } }' \`\`\` #### Token activation via A2A \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 7, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-7", "parts": \[ { "data": { "input": "https://example.com", "token": "YOUR\_TOKEN", "memo": "YOUR\_PAYMENT\_MEMO" }, "mediaType": "application/json" } \] } } }' \`\`\` > \*\*Note on token activation:\*\* Pass \`token\` and \`memo\` inside a \`data\` Part or as JSON inside a \`text\` Part. A2A does not use HTTP-level Authorization headers. #### Multi-turn conversation (follow-up) \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 4, "method": "SendMessage", "params": { "contextId": "ctx-uuid-1", "message": { "messageId": "msg-uuid-4", "parts": \[ { "text": "Now convert the tables to JSON" } \] } } }' \`\`\` #### GetTask \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 5, "method": "GetTask", "params": { "id": "task\_12345" } }' \`\`\` #### ListTasks \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 6, "method": "ListTasks", "params": { "contextId": "ctx\_12345", "pageSize": 10 } }' \`\`\` ### A2A Message Parts Messages use the A2A \`Part\` format (field-name discriminators per spec v1.0.0): | Type | Description | Fields | | ------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- | | \`text\` | Plain text content or JSON-encoded params | \`text\` | | \`raw\` | File content as base64 bytes; normalized to a data URI (\`data:;base64,\`) for the core | \`raw\` (base64), \`mediaType\` (optional) | | \`data\` | Structured JSON data (core params) | \`data\` (object), \`mediaType\` (optional) | | \`url\` | URL to fetch and convert | \`url\` (http/https) | \*\*Part → Core Parameter Mapping:\*\* - \`text\` Part → \`input\` param (direct content) or JSON-encoded params (\`{ "input": "...", "prompt": "..." }\`) - \`data\` Part → merged as params (\`input\`, \`prompt\`, \`result\`, \`token\`, \`memo\`, etc.) - \`url\` Part → \`input\` param (fetched and converted) ### Message Object \`\`\`typescript interface Message { messageId: string; // REQUIRED: unique ID (e.g. "msg-uuid") contextId?: string; // Optional: group related tasks taskId?: string; // Optional: associate with existing task role: "user" | "agent"; // REQUIRED parts: Array; // REQUIRED: at least one part } \`\`\` ### Task Data Model \`\`\`typescript interface Task { id: string; // "task\_\_" contextId: string; // "ctx\_\_" status: { state: string; // "TASK\_STATE\_WORKING" | "TASK\_STATE\_COMPLETED" | "TASK\_STATE\_FAILED" | "TASK\_STATE\_CANCELED" | "TASK\_STATE\_REJECTED" timestamp: string; // ISO 8601 message?: Message; // only on failure }; artifacts?: Array<{ artifactId: string; name: string; parts: Array; }>; history?: Array; } \`\`\` ### Task States | State | Description | | ---------------------- | ----------------------------- | | \`TASK\_STATE\_WORKING\` | Task is being processed | | \`TASK\_STATE\_COMPLETED\` | Task finished successfully | | \`TASK\_STATE\_FAILED\` | Task failed during processing | | \`TASK\_STATE\_CANCELED\` | Task was canceled by client | | \`TASK\_STATE\_REJECTED\` | Task was rejected by server | ### Error Responses A2A uses JSON-RPC 2.0 error format with A2A-specific error codes: | Code | Error | Description | | -------- | --------------------------------- | ---------------------------------------------------------- | | \`-32700\` | Parse error | Invalid JSON payload | | \`-32600\` | Invalid Request | Missing required fields (message.parts, message.messageId) | | \`-32601\` | Method not found | Unknown A2A method | | \`-32001\` | Task not found | Task ID does not exist | | \`-32002\` | Task is not in a cancelable state | CancelTask on a terminal task | Unsupported media type is returned as HTTP \*\*415\*\* (not a JSON-RPC error code). \*\*Example error response:\*\* \`\`\`json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Invalid Request", "data": \[ { "@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": \[ { "field": "message.messageId", "description": "Message messageId is required" } \] } \] } } \`\`\` ### Streaming Use \`SendStreamingMessage\` for real-time SSE updates: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -H "Accept: text/event-stream" -d '{ "jsonrpc": "2.0", "id": 5, "method": "SendStreamingMessage", "params": { "message": { "messageId": "msg-uuid-5", "parts": \[ { "text": "Convert https://example.com" } \] } } }' \`\`\` Response format (A2A v1.0.0 streaming sequence - exact frames the service emits): \`\`\` data: {"jsonrpc":"2.0","id":5,"result":{"task":{"id":"task\_...","contextId":"ctx\_...","status":{"state":"TASK\_STATE\_WORKING","timestamp":"..."},"artifacts":\[\]}}} data: {"jsonrpc":"2.0","id":5,"result":{"artifactUpdate":{"taskId":"task\_...","contextId":"ctx\_...","artifact":{"artifactId":"artifact\_...","name":"conversion\_result","parts":\[{"text":" partial "}\]},"append":true,"lastChunk":false}}} data: {"jsonrpc":"2.0","id":5,"result":{"artifactUpdate":{"taskId":"task\_...","contextId":"ctx\_...","artifact":{"artifactId":"artifact\_...","name":"conversion\_result","parts":\[{"text":" more "}\]},"append":true,"lastChunk":true}}} data: {"jsonrpc":"2.0","id":5,"result":{"statusUpdate":{"taskId":"task\_...","contextId":"ctx\_...","status":{"state":"TASK\_STATE\_COMPLETED","timestamp":"..."}}}} data: \[DONE\] \`\`\` The first frame carries the full \`task\` in TASK\_STATE\_WORKING; content streams as \`artifactUpdate\` frames (\`lastChunk: true\` on the final chunk); \`statusUpdate\` closes the stream with the terminal TASK\_STATE\_COMPLETED state. The persisted task (via \`GetTask\`) carries the real \`artifacts\[\].parts\[\]\` content. ### Subscribe to Task Subscribe to an existing task for real-time updates: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -H "Accept: text/event-stream" -d '{ "jsonrpc": "2.0", "id": 6, "method": "SubscribeToTask", "params": { "id": "task\_12345" } }' \`\`\` ### Cancel Task Cancel an in-progress task: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 7, "method": "CancelTask", "params": { "id": "task\_12345" } }' \`\`\` Response: \`\`\`json { "jsonrpc": "2.0", "id": 7, "result": { "id": "task\_12345", "contextId": "ctx\_...", "status": { "state": "TASK\_STATE\_CANCELED", "timestamp": "2026-06-26T16:00:00.000Z" } } } \`\`\` ## ACP Configuration Connect mdapi.io to your IDE or coding agent (JetBrains, Cursor, VS Code, etc.) via the Agent Client Protocol v1.0.0. ACP is a JSON-RPC 2.0 endpoint at \`POST /acp\`. Its native surface is \*\*session/turn\*\*: create an ephemeral session, then send a prompt - the converted content streams back as \`session/update\` notifications. > \*\*Single source via \`input\`:\*\* each \`session/prompt\` carries a unified \`input\` - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). ### Basic Configuration Add to your ACP client configuration (IDE plugin / agent settings): \`\`\`json { "acpServers": { "mdapi": { "url": "https://mdapi.io/acp" } } } \`\`\` Or call the JSON-RPC endpoint directly with \`POST /acp\` (Content-Type: \`application/json\`). Sessions are \*\*stateless and ephemeral\*\* - nothing is persisted server-side, so each \`session/prompt\` runs as an independent conversion: \`\`\`bash # 1. Create a session curl -X POST https://mdapi.io/acp \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":1,"method":"session/new","params":{}}' # 2. Send a prompt (content streams back as session/update notifications) curl -X POST https://mdapi.io/acp \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{ "sessionId":"", "prompt":\[{"type":"text","text":""}, {"type":"resource\_link","uri":"https://example.com"}\] }}' \`\`\` > \*\*Note:\*\* \`GET /acp\` is not supported (ACP is POST-only). A free tier is available without a token. ### ACP Methods | Method | Description | | ---------------- | ------------------------------------------------------------------------------- | | \`initialize\` | Handshake: return protocol version, agent capabilities, and agent info | | \`session/new\` | Create an ephemeral, stateless session (returns a \`sessionId\`) | | \`session/prompt\` | Run a conversion turn; content streams back as \`session/update\` notifications | | \`session/cancel\` | Notification (204, no response body) that best-effort cancels an in-flight turn | ### ACP Session Examples \*\*URL conversion (instructions + \`resource\_link\`):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[ { "type": "text", "text": "Summarize" }, { "type": "resource\_link", "uri": "https://example.com" } \] } } \`\`\` \*\*Text conversion (a bare \`text\` block becomes the input):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[{ "type": "text", "text": "Hello World" }\] } } \`\`\` \*\*File conversion (a \`data:\` URI in a \`text\` block is auto-detected as a file):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 3, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[ { "type": "text", "text": "Extract the title" }, { "type": "text", "text": "data:text/plain;base64,SGVsbG8gV29ybGQ=" } \] } } \`\`\` \*\*Token activation (first request only):\*\* every protocol passes the token per-call. Include it on any method's \`params\` as a \`token\` (+ \`memo\` on first use) - ACP does not use a session-level authenticate exchange. ### ACP Response Format Content is delivered \*\*exclusively via \`session/update\` notifications\*\*; the final \`session/prompt\` result carries only the \`stopReason\` (\`PromptResponse\`). A successful turn looks like this SSE stream: \`\`\` data: {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"","update":\[{"content":{"type":"text","text":""},"messageId":""}\]}} data: {"jsonrpc":"2.0","id":1,"result":{"stopReason":"end\_turn"}} data: \[DONE\] \`\`\` - The \`session/update\` \`update\[\]\` array holds the content chunks; \`messageId\` is stable across all chunks of a single turn. - \*\*Streaming:\*\* add \`"stream": true\` to the \`session/prompt\` \`params\` to stream the conversion live - each content chunk arrives as its own \`session/update\` notification (same \`messageId\`), then the \`PromptResponse\`. Without it, the whole turn arrives as one buffered \`session/update\` notification. Streaming is inherent to ACP; content only ever rides notifications. - A failed conversion streams an explanatory \`session/update\`, then ends with \`stopReason: "error"\`. \`session/cancel\` is a client→agent notification - the agent acknowledges it with HTTP 204 and no body. - Token status (\`X-Token-Balance\`, \`X-Token-Expires\`, \`X-Token-Status\`) is proxied from the core into the response headers. ## Usage scenarios mdapi.io is a minimal, self-documenting service-transport primitive: REST, MCP, ACP, A2A, and OpenAI-compatible endpoints all call the same transformation core, so agents can combine protocols and pass already-processed knowledge between each other. - \*\*Agent swarms\*\* - each request is handled by a stateless, automatically-scaled execution environment, so the service scales horizontally. An orchestrator fans work out across a swarm of agents, and the swarm processes very large batches of distinct resources in parallel - millions of resources in a matter of minutes, the ceiling set by how widely the work is distributed rather than by the service. Different users may freely access the same resource. - \*\*Shared vs individual payment\*\* - an orchestrator can pay once for a shared token (batching on-chain activity), or each agent can activate its own token for the exact volume it received. - \*\*Human-in-the-loop\*\* - if an agent has no wallet or insufficient funds, it returns payment details + a QR code; the human pays from a mobile device and the agent resumes. - \*\*Role switching\*\* - an agent's role can change mid-task; one agent fetches/normalizes, another summarizes/extracts, relaying compact results via the text or prompt parameters. - \*\*Cross-protocol interoperability\*\* - knowledge extracted on one protocol is reusable on another. - \*\*Bulk processing / model training\*\* - the swarm pattern turns mdapi.io into a high-throughput edge pipeline for large corpora. See https://mdapi.io/about for the full scenario walkthrough. ## Links - \*\*About service:\*\* https://mdapi.io/about - \*\*API docs:\*\* https://mdapi.io - \*\*MCP server manifest:\*\* https://mdapi.io/mcp - \*\*Health check:\*\* https://mdapi.io/health - \*\*API documentation:\*\* https://mdapi.io/llms.txt - \*\*Full API documentation:\*\* https://mdapi.io/llms-full.txt - \*\*AI discovery:\*\* https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json - \*\*AI Agent discovery:\*\* https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json - \*\*A2A Agent card:\*\* https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json - \*\*ACP manifest:\*\* https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json - \*\*x402 payment manifest:\*\* https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json - \*\*OpenAPI specification (JSON):\*\* https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json - \*\*OpenAPI specification (YAML):\*\* https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml - \*\*MAPI specification (case-insensitive path MAPI.md support):\*\* https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md - \*\*Skill specification (case-insensitive path SKILL.md support):\*\* https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md - \*\*Agent Plugins package (agent-plugins.org v1.0.0):\*\* https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/ ## External Links - \*\*github.com\*\* https://github.com/mdapiio/mdapi.io - \*\*skills.sh\*\* https://www.skills.sh/mdapiio/mdapi.io - \*\*skillsmp.com\*\* https://skillsmp.com/creators/mdapiio/mdapi.io - \*\*clawhub.ai\*\* https://clawhub.ai/mdapiio - \*\*x.com\*\* https://x.com/mdapiio ## Disclaimer \*\*The service is provided "AS IS".\*\* > mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.
\# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems. Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage. ## Agent entrypoint - \*\*Start AI discovery\*\* → https://mdapi.io/.well-known/ai-discovery.json - \*\*Use skill\*\* → https://mdapi.io/.well-known/skill.md ## Quick Start Choose your entry point based on your role: | Role | Protocol | Endpoint | When to use | | ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). | | AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. | | AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. | | OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. | | Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. | ### Universal discovery All protocols and capabilities are described in one file: GET /.well-known/ai-discovery.json ## Features - Stateless, in-memory processing - Edge execution with automatic scaling - Prompt-driven transformation - AI-optimized output for LLMs - Pay-per-use via x402 v1/v2 or manual payment ## Supported Formats | Type | Formats | | --------- | ------------------------------ | | Documents | PDF, DOCX, XLSX, XLS, ODT, ODS | | Images | JPEG, JPG, PNG, WebP, SVG | | Text | HTML, XML, JSON, CSV, TXT | | Webpages | Any publicly accessible URL | ## Source Parameters (all protocols) Every protocol (REST, MCP, ACP, A2A, OpenAI) converges on the \*\*same conversion core\*\*, so content is specified via a single unified \`input\` parameter everywhere. | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | \`input\` | string | Content to convert: a URL (http(s)://...), a data URI (data:mime/type;base64,...), or plain text. Auto-detected: http(s):// → URL, data: → file, otherwise → text. | The \`input\` parameter is auto-detected by the core: URLs (starting with \`http://\` or \`https://\`) are fetched, data URIs (starting with \`data:\`) are decoded as files, and anything else is treated as raw text. Additional parameters (\`prompt\`, \`result\`, \`stream\`, \`token\`, \`memo\`) are orthogonal and may be combined with \`input\`. All five protocols expose the same \`input\` source and apply the same transformations, streaming, and prompt-driven processing - the only difference is the transport (REST query/JSON, MCP \`tools/call\`, ACP \`session/prompt\`, OpenAI \`messages\`, A2A \`message.parts\`). ## Limits | Limit | Value | | ------------------- | -------------------------------------------------------------------------------- | | \*\*Max file size\*\* | 50 MB | | \*\*Max URL content\*\* | 50 MB | | \*\*Rate limit\*\* | 10,000 requests per hour | | \*\*Free tier\*\* | 10 requests per day (no token required), within the service’s overall free quota | | \*\*Paid tier\*\* | min $0.01 per conversion (USDC on Solana) | | \*\*Token validity\*\* | 1 year | ## Authentication \*\*Recommended:\*\* Use \`Authorization: Bearer TOKEN\` | Method | Use Case | | -------------------- | ---------------------------------- | | Bearer (recommended) | \`-H "Authorization: Bearer TOKEN"\` | | Header | \`-H "X-Token-Required: TOKEN"\` | ## Token Activation Before you can use a paid token, you must receive a 402 response first: | Step | Description | | ---- | ------------------------------------------------------- | | 1 | Request without token → Receive 402 with NEW token+memo | | 2 | Send USDC on Solana to wallet with memo from 402 | | 3 | Retry with EXACT token+memo from 402 → Activation | | 4 | After: use token only (no memo needed) | Important: The token+memo issued in the 402 response MUST be used exactly. Using old token or different memo will be rejected. ## API Usage ### GET / (Content conversion) Simple content conversion using query parameters. Returns Markdown directly. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \`input\` | string | \* | Content to convert (URL, text, or data URI - auto-detected) | | \`prompt\` | string | | Custom instructions for LLM processing | | \`result\` | string | | Response format: \`markdown\`, \`prompt\`, or \`both\` | | \`stream\` | boolean | | Enable streaming: true for SSE response | | \`token\` | string | | Access token for paid tier | | \`memo\` | string | | Memo for token activation | \*The \`input\` parameter is required.\* > \*\*⚠️ Browser URL limit:\*\* GET requests with long \`input\` or \`prompt\` values may exceed browser URL limits (~2048 characters). Use POST with JSON body for large payloads. ### POST / (Content conversion via JSON) Supports content conversion via JSON body. The \`input\` parameter accepts URLs, text, or data URIs (auto-detected). Returns a JSON object containing the Markdown content. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \`input\` | string | \* | Content to convert (URL, text, or data URI - auto-detected) | | \`prompt\` | string | | Custom instructions for LLM processing | | \`result\` | string | | Response format: \`markdown\`, \`prompt\`, or \`both\` | | \`stream\` | boolean | | Enable streaming: true for SSE response | | \`token\` | string | | Access token for paid tier | | \`memo\` | string | | Memo for token activation | \*The \`input\` parameter is required.\* ### Result Format Parameter The \`result\` parameter controls the response format for both GET and POST requests. | Value | Description | | -------------------- | --------------------------------------------------------------- | | \`markdown\` (default) | Returns the converted Markdown content | | \`prompt\` | Returns the result of LLM processing with \`prompt\` instructions | | \`both\` | Returns both \`markdown\` and \`prompt\_result\` in the response | When \`result=both\`: - \*\*GET requests\*\* return Markdown combining \`markdown\`, followed by "## Prompt Result" and \`prompt\_result\` (always in Markdown format) - \*\*POST requests\*\* return JSON with \`markdown\` and \`prompt\_result\` fields > \*\*Auto \`result\`:\*\* When \`prompt\` is provided without an explicit \`result\`, the core automatically sets \`result="prompt"\` (LLM output only). Without \`prompt\`, default is \`result="markdown"\`. Only specify \`result\` explicitly when you need both (\`result="both"\`). ### Prompt Parameter The \`prompt\` parameter lets you specify custom instructions for the LLM to follow when generating the result. | Use Case | Example | | ------------------ | ---------------------------------------------------------- | | Summarize | \`?input=https://example.com&prompt=Summarize\` | | Extract key points | \`?input=Hello World&prompt=Extract key points\` | | Convert to JSON | \`?input=https://example.com&prompt=Convert to JSON format\` | | Analyze content | \`?input=Hello World&prompt=Analyze and explain\` | ### Streaming Parameter The \`stream\` parameter enables Server-Sent Events (SSE) streaming for real-time response delivery. \*\*Type:\*\* \`boolean\` \*\*Default:\*\* \`false\` (non-streaming) Example: \`\`\`bash curl "https://mdapi.io/?input=...&stream=true" \`\`\` Response format (OpenAI-compatible SSE, one JSON object per \`data:\` line): \`\`\`json data: {"type":"token\_info","token\_status":"valid","token\_balance":0.99,"token\_expires":1798761600} data: {"choices":\[{"index":0,"delta":{"content":" chunk"},"finish\_reason":null}\]} data: {"choices":\[{"index":0,"delta":{"content":" more"},"finish\_reason":null}\]} data: {"choices":\[{"index":0,"delta":{},"finish\_reason":"stop"}\]} data: \[DONE\] \`\`\` \*\*Native streaming per protocol.\*\* Every protocol delivers a \*real\* content stream when \`stream: true\`, but each emits it in its own native frame format (so existing clients keep working): | Protocol | Streaming frame format | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | REST | OpenAI-compatible \`choices/delta\` frames | | OpenAI | \`chat.completion.chunk\` (\`choices/delta\`) | | MCP | \`notifications/message\` content chunks, then one final \`tools/call\` result frame | | ACP | \`session/update\` notification chunks (one stable \`messageId\` per turn), then a final response carrying only \`stopReason\` | | A2A | \`result.task\` (\`TASK\_STATE\_WORKING\`) start frame, \`result.artifactUpdate\` (\`{artifact, append, lastChunk}\`) content frames, then \`result.statusUpdate\` (\`TASK\_STATE\_COMPLETED\`) - stream closes | > \*\*Note on MCP transport vs. the \`stream\` parameter.\*\* The MCP manifest advertises \`transport.type: "streamable-http"\` - that is the MCP \*transport\* > (how JSON-RPC requests are delivered to \`POST /mcp\`). It is unrelated to the \`stream\` \*parameter\*, which independently enables SSE streaming of the > conversion \*\*content\*\*. You can use MCP without streaming; and when you do pass \`stream: true\`, the content arrives as SSE frames alongside the transport. ### Response Codes | Code | Description | Response Body (GET) | Response Body (POST) | | ---- | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | | 200 | Success | Markdown content | JSON with \`success\`, markdown, prompt\_result, metrics, token fields | | 402 | Payment Required | Markdown payment instructions (\`X-Error-Code: payment\_required\`) | JSON with \`success:false\`, \`code:"payment\_required"\`, and payment object | | 400 | Bad Request | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"invalid\_request"}\` | | 401 | Invalid Token | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"unauthorized"}\` | | 404 | Not Found | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"not\_found"}\` | | 413 | Payload Too Large | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"too\_large"}\` | | 429 | Rate Limited | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"rate\_limited"}\` | | 500 | Server Error | Markdown error + \`X-Error-Code\` header | JSON \`{"success":false,"error":"...","code":"server\_error"}\` | ### Token Status The X-Token-Status header (and token\_status field in responses) indicates the current state of authentication: | Status | Description | | ------------------ | --------------------------------------------------------------------------------------- | | free | Free tier (no token required, 10 requests/day), within the service's overall free quota | | valid | Paid token active with remaining balance | | invalid | Token not found or not provided | | expired | Token validity period has ended | | exhausted | Token balance has been fully used | | expired\_pending | Activation memo has expired | | activated | Token was just activated with this request | | verification\_error | Payment verification failed | | invalid\_payment | Payment transaction is invalid | | error | Internal error during token processing | | pending | Payment required (token not yet activated) | ### Endpoints | Method | Path | Description | | ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | GET | /about | About service | | GET | / | API docs or conversion (via query parameter: \`input\`, \`prompt\`, \`result\`) | | POST | / | Convert content via JSON body (supports \`input\`, \`prompt\`, \`result\` params) | | POST | /v1/chat/completions | OpenAI-compatible endpoint | | GET | /mcp | MCP server manifest | | POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) | | POST | /acp | ACP RPC endpoint (IDE agents) | | POST | /a2a | A2A RPC endpoint (agent2agent) | | GET | /health | Health check | | GET | /llms.txt | API documentation | | GET | /llms-full.txt | Full API documentation | | GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery | | GET | /.well-known/agent.json or /agent.json | AI Agent discovery | | GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card | | GET | /.well-known/acp.json or /acp.json | ACP manifest | | GET | /.well-known/x402.json or /x402.json | x402 payment manifest | | GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) | | GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) | | GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) | | GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) | | GET | /.well-known/skills/index.json | Legacy skills index | | GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) | | GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) | | GET | /.well-known/mcp/server-cards.json | MCP server cards index | | GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest | | GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config | | GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill | #### Examples \`\`\`bash # URL conversion via GET (free) curl "https://mdapi.io/?input=https://example.com" # URL with prompt and result=both (returns markdown + prompt\_result) curl "https://mdapi.io/?input=https://example.com&prompt=Summarize&result=both" # Text with prompt (auto result=prompt) curl "https://mdapi.io/?input=Hello World&prompt=Summarize" # Token activation via GET (activate and use) curl -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" "https://mdapi.io/?input=https://example.com" # Paid request with token via GET (using token) curl -H "Authorization: Bearer YOUR\_TOKEN" "https://mdapi.io/?input=https://example.com" # URL conversion via POST (free) curl -X POST -H "Content-Type: application/json" -d '{"input":"https://example.com"}' "https://mdapi.io/" # Text with prompt via POST curl -X POST -H "Content-Type: application/json" -d '{"input":"Hello World","prompt":"Summarize","result":"both"}' "https://mdapi.io/" # File upload via POST (data URI) curl -X POST -H "Content-Type: application/json" -d '{"input":"data:text/plain;base64,SGVsbG8gV29ybGQ="}' "https://mdapi.io/" # Token activation via POST curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" -d '{"input":"https://example.com"}' "https://mdapi.io/" # Paid request with token via POST curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer YOUR\_TOKEN" -d '{"input":"https://example.com"}' "https://mdapi.io/" \`\`\` ### OpenAI Compatible Endpoint The \`/v1/chat/completions\` endpoint provides an OpenAI‑compatible API for markdown conversion with streaming support. \*\*Supported features:\*\* - URL extraction from message content (any text containing https?://) - image\_url in messages (OpenAI format) - supports HTTP URLs and data URLs - file in messages (OpenAI format) - base64 encoded files (field \`file.data\`, optional \`file.mimeType\`; built into a \`data:\` URI for the core - no \`file.filename\` required or used) - Direct text content in messages (any text without a URL is sent to the core as the \`input\` source and converted to Markdown) - Token and memo via headers (recommended for POST) - Streaming SSE responses (\`stream: true\`) - Custom instructions with LLM processing (system messages, or user messages containing instruction keywords such as \*extract, summarize, analyze, format, convert to, write as, create, generate, json\* → LLM-driven summary/extraction/transformation) - \`prompt\` for LLM-processed output. The response surfaces \`prompt\_result\` at the top level alongside the standard \`choices\[\].message.content\` (which carries \`prompt\_result\` when prompt is set, otherwise the Markdown). \`model\` is accepted but not required (any string; the service uses its own conversion pipeline, not a remote LLM chat model, unless custom instructions trigger LLM processing). > \*\*Content via message text:\*\* the message text is passed to the core as the \`input\` parameter - the same unified source as every other protocol. URLs are auto-detected, data URIs are decoded as files, and plain text is processed directly. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). #### Request Schema \`\`\`json { "type": "object", "properties": { "model": { "type": "string", "description": "Optional model identifier (any string accepted; not required)" }, "messages": { "type": "array", "items": { "type": "object", "properties": { "role": {"type": "string", "enum": \["user", "system", "assistant"\]}, "content": {"oneOf": \[{"type": "string"}, {"type": "array"}\]} } }, "description": "Chat messages. URL in content, image\_url or file in content for input" }, "stream": { "type": "boolean", "default": false, "description": "Enable streaming SSE responses" }, "prompt": { "type": "string", "description": "Custom instructions for LLM processing (alternative to instruction keywords in messages)" }, "result": { "type": "string", "enum": \["markdown", "prompt", "both"\], "description": "Response format when using prompt" }, "token": {"type": "string", "description": "Access token for paid tier"}, "memo": {"type": "string", "description": "Memo for token activation"}, "input": { "type": "string", "description": "Content to convert (URL, text, or data URI - auto-detected). Alternative to a URL/file embedded in messages" } }, "required": \["messages"\] } \`\`\` > \*\*Note on streaming + custom instructions:\*\* when custom instructions trigger LLM processing, the response is returned as a single completion (streaming is not applied to the LLM pass). Streaming SSE applies to the standard conversion path. ## MCP Configuration Connect mdapi.io to your MCP-compatible client (spec 2026-07-28, stateless). > \*\*Single source via \`input\`:\*\* the \`convert\` tool accepts a unified \`input\` parameter - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). ### Protocol Requirements - \*\*Transport:\*\* Streamable HTTP (POST-only for JSON-RPC, GET for manifest) - \*\*Required headers:\*\* \`MCP-Protocol-Version: 2026-07-28\` and \`Mcp-Method\` on every request; \`Mcp-Name\` additionally on \`tools/call\`, \`resources/read\`, and \`prompts/get\` - \*\*Stateless:\*\* No sessions - every request is independent - \*\*Discovery:\*\* Use \`server/discover\` to query server capabilities and supported versions ### Basic Configuration Add to your MCP config file: \`\`\`json { "mcpServers": { "mdapi": { "url": "https://mdapi.io/mcp" } } } \`\`\` > \*\*Note:\*\* No token is required to connect. A free tier is available (10 requests per day), within the service’s overall free quota. ### OpenClaw Integration OpenClaw can use mdapi.io in two ways: \*\*Option 1: Via MCP (Recommended)\*\* \`\`\`json { "mcpServers": { "mdapi": { "url": "https://mdapi.io/mcp" } } } \`\`\` \*\*Option 2: Via OpenAI-compatible endpoint\*\* \`\`\`bash openclaw config set llm.apiBase https://mdapi.io/v1 openclaw config set llm.apiKey YOUR\_TOKEN \`\`\` ### Using MCP with a token MCP does not use HTTP-level Authorization headers. The token is always passed inside the tool \`arguments\` object. \*\*Activation\*\* - include \`token\` + \`memo\` in the first request: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "token": "YOUR\_TOKEN", "memo": "YOUR\_PAYMENT\_MEMO" } } } \`\`\` \*\*After activation\*\* - use \`token\` only (no memo needed): \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "token": "YOUR\_ACTIVATED\_TOKEN" } } } \`\`\` ### MCP Tool Examples Convert with prompt and result: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "prompt": "Summarize", "result": "both", "token": "YOUR\_TOKEN" } } } \`\`\` Process text directly: \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "Hello World", "prompt": "Extract key points", "result": "prompt" } } } \`\`\` Stream with SSE (native MCP frames): \`\`\`json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "convert", "arguments": { "input": "https://example.com", "stream": true } } } \`\`\` Response (SSE over \`streamable-http\`): intermediate \`notifications/message\` content chunks, then one final \`tools/call\` result frame with the full Markdown: \`\`\` data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":" partial "}} data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":" more "}} data: {"jsonrpc":"2.0","id":3,"result":{"content":\[{"type":"text","text":""}\],"isError":false}} data: \[DONE\] \`\`\` ### Using Environment Variables Or use environment variable: \`\`\`bash export MDAPI\_TOKEN=YOUR\_ACTIVATED\_TOKEN \`\`\` ## Code Examples ### JavaScript (fetch) \`\`\`javascript // Convert a URL via GET - returns Markdown directly const response = await fetch('https://mdapi.io/?input=https://example.com'); const markdown = await response.text(); console.log(markdown); \`\`\` \`\`\`javascript // Convert a URL via POST - returns JSON with metadata const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); console.log(data.markdown); \`\`\` \`\`\`javascript // Text with prompt - returns prompt\_result const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'Hello World', prompt: 'Summarize', result: 'both' }) }); const data = await response.json(); console.log(data.markdown); console.log(data.prompt\_result); \`\`\` \`\`\`javascript // File upload via data URI const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'data:text/plain;base64,SGVsbG8gV29ybGQ=' }) }); const data = await response.json(); console.log(data.markdown); \`\`\` \`\`\`javascript // Token activation - first request with token + memo const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\_TOKEN', 'X-Memo-Required': 'YOUR\_MEMO' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); // After activation, use token only (no memo needed) \`\`\` \`\`\`javascript // Streaming via OpenAI-compatible endpoint const response = await fetch('https://mdapi.io/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\_TOKEN' }, body: JSON.stringify({ model: 'markdown-v1', messages: \[{ role: 'user', content: 'Convert https://example.com' }\], stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; process.stdout.write(decoder.decode(value)); } \`\`\` ### Python \`\`\`python import requests # Convert a URL via GET - returns Markdown directly response = requests.get('https://mdapi.io/?input=https://example.com') response.raise\_for\_status() print(response.text) \`\`\` \`\`\`python import requests # Convert a URL via POST - returns JSON with metadata response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) \`\`\` \`\`\`python import requests # Text with prompt - returns prompt\_result response = requests.post( 'https://mdapi.io/', json={'input': 'Hello World', 'prompt': 'Summarize', 'result': 'both'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) print(data\['prompt\_result'\]) \`\`\` \`\`\`python import requests # File upload via data URI with open('document.pdf', 'rb') as f: import base64 file\_data = base64.b64encode(f.read()).decode() response = requests.post( 'https://mdapi.io/', json={'input': f'data:text/plain;base64,{file\_data}'} ) response.raise\_for\_status() data = response.json() print(data\['markdown'\]) \`\`\` \`\`\`python import requests # Token activation response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'}, headers={ 'Authorization': 'Bearer YOUR\_TOKEN', 'X-Memo-Required': 'YOUR\_MEMO' } ) response.raise\_for\_status() # After activation, use token only (no memo needed) \`\`\` \`\`\`python from openai import OpenAI # OpenAI-compatible streaming client = OpenAI(base\_url='https://mdapi.io/v1', api\_key='YOUR\_TOKEN') stream = client.chat.completions.create( model='markdown-v1', messages=\[{'role': 'user', 'content': 'Convert https://example.com'}\], stream=True ) for chunk in stream: if chunk.choices\[0\].delta.content: print(chunk.choices\[0\].delta.content, end='') \`\`\` ### Go \`\`\`go package main import ( "fmt" "io" "net/http" ) func main() { // Convert a URL via GET - returns Markdown directly resp, err := http.Get("https://mdapi.io/?input=https://example.com") if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() body, \_ := io.ReadAll(resp.Body) fmt.Println(string(body)) } \`\`\` \`\`\`go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { // Convert a URL via POST - returns JSON with metadata payload, \_ := json.Marshal(map\[string\]string{ "input": "https://example.com", }) resp, err := http.Post( "https://mdapi.io/", "application/json", bytes.NewReader(payload), ) if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() var result map\[string\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\["markdown"\]) } \`\`\` \`\`\`go package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "net/http" "os" ) func main() { // File upload via data URI fileBytes, \_ := os.ReadFile("document.pdf") b64 := base64.StdEncoding.EncodeToString(fileBytes) payload, \_ := json.Marshal(map\[string\]string{ "input": "data:text/plain;base64," + b64, }) resp, err := http.Post( "https://mdapi.io/", "application/json", bytes.NewReader(payload), ) if err != nil { fmt.Println("HTTP error:", err) return } defer resp.Body.Close() var result map\[string\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\["markdown"\]) } \`\`\` ### Rust \`\`\`rust use anyhow::Result; use reqwest::Client; #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let url = "https://mdapi.io/?input=https://example.com"; let response = client.get(url).send().await?; response.error\_for\_status\_ref()?; let markdown = response.text().await?; println!("{}", markdown); Ok(()) } \`\`\` \`\`\`rust use anyhow::{Result, Context}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #\[derive(Deserialize)\] struct ApiResponse { markdown: Option, prompt\_result: Option, } #\[derive(Serialize)\] struct ConvertRequest { input: String, #\[serde(skip\_serializing\_if = "Option::is\_none")\] prompt: Option, #\[serde(skip\_serializing\_if = "Option::is\_none")\] result: Option, } pub struct MdApiClient { client: Client, base\_url: String, token: Option, } impl MdApiClient { pub fn new(token: Option) -> Self { Self { client: Client::new(), base\_url: "https://mdapi.io".to\_string(), token, } } pub async fn convert\_url( &self, url: &str, prompt: Option<&str>, ) -> Result { let body = ConvertRequest { input: url.to\_string(), prompt: prompt.map(|p| p.to\_string()), result: prompt.map(|\_| "both".to\_string()), }; let mut request = self.client .post(&self.base\_url) .header("Content-Type", "application/json") .json(&body); if let Some(ref token) = self.token { request = request.header("Authorization", format!("Bearer {}", token)); } let response = request .send() .await .context("Failed to send HTTP request")?; response .error\_for\_status\_ref() .context("API returned error status")?; response .text() .await .context("Failed to read response body") } } // Example: convert a URL with prompt #\[tokio::main\] async fn main() -> Result<()> { let token = std::env::var("MDAPI\_TOKEN").ok(); let client = MdApiClient::new(token); let markdown = client .convert\_url("https://example.com", Some("Summarize")) .await?; println!("{}", markdown); Ok(()) } \`\`\` \`\`\`rust use anyhow::Result; use reqwest::Client; use base64::engine::general\_purpose::STANDARD; use base64::Engine; #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let file\_bytes = std::fs::read("document.pdf")?; let b64 = STANDARD.encode(&file\_bytes); let body = serde\_json::json!({ "input": format!("data:text/plain;base64,{}", b64) }); let response = client .post("https://mdapi.io/") .header("Content-Type", "application/json") .json(&body) .send() .await?; let data: serde\_json::Value = response.json().await?; println!("{}", data\["markdown"\]); Ok(()) } \`\`\` \`\`\`rust use anyhow::Result; use futures\_util::stream::StreamExt; use reqwest::Client; // Streaming response when using the OpenAI‑compatible endpoint with stream = true #\[tokio::main\] async fn main() -> Result<()> { let client = Client::new(); let token = std::env::var("MDAPI\_TOKEN")?; let body = serde\_json::json!({ "model": "markdown-v1", "messages": \[ { "role": "user", "content": "Convert https://example.com" } \], "stream": true }); let response = client .post("https://mdapi.io/v1/chat/completions") .header("Authorization", format!("Bearer {}", token)) .json(&body) .send() .await?; response.error\_for\_status\_ref()?; let mut stream = response.bytes\_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; let text = String::from\_utf8\_lossy(&chunk); eprint!("{}", text); } Ok(()) } \`\`\` ### OpenAI SDK \`\`\`python from openai import OpenAI client = OpenAI( base\_url="https://mdapi.io/v1", api\_key="YOUR\_TOKEN" ) try: response = client.chat.completions.create( model="markdown-v1", messages=\[{"role": "user", "content": "Convert https://example.com"}\] ) print(response.choices\[0\].message.content) except Exception as e: print("API error:", e) \`\`\` #### OpenAI with paid token \`\`\`bash curl -X POST "https://mdapi.io/v1/chat/completions" -H "Authorization: Bearer YOUR\_TOKEN" -H "X-Memo-Required: YOUR\_MEMO" -H "Content-Type: application/json" -d '{"model":"markdown-v1","messages":\[{"role":"user","content":"Convert https://example.com"}\]}' \`\`\` After activation, use token only (no memo needed): \`\`\`bash curl -X POST "https://mdapi.io/v1/chat/completions" -H "Authorization: Bearer YOUR\_ACTIVATED\_TOKEN" -H "Content-Type: application/json" -d '{"model":"markdown-v1","messages":\[{"role":"user","content":"Convert https://example.com"}\]}' \`\`\` ## A2A Configuration Connect mdapi.io to your A2A-compatible agent (Claude Code, Codex, OpenClaw, Hermes, etc.). ### Basic Configuration Add to your A2A client configuration: \`\`\`json { "agent": { "name": "mdapi", "agentCard": { "url": "https://mdapi.io/.well-known/agent-card.json" } } } \`\`\` Or use JSON-RPC directly: \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-1", "parts": \[ { "text": "Convert https://example.com" } \] } } } \`\`\` ### A2A Methods | Method | Description | | -------------------- | --------------------------------------- | | SendMessage | Send a message to initiate conversion | | SendStreamingMessage | Send message with SSE streaming updates | | GetTask | Get task status and results by ID | | ListTasks | List tasks with optional filtering | | CancelTask | Cancel an in-progress task | | SubscribeToTask | Subscribe to task updates via SSE | > \*\*Single source via \`input\`:\*\* the \`input\` parameter in the message parts is the unified source - the same as the REST endpoint. A bare URL > inside a text part (e.g. \`"Convert https://example.com"\`) is extracted automatically and used as the conversion source, so you don't need to wrap it > in structured JSON. Instructions such as \`Summarize\` should be passed via the structured \`{ "input": "...", "prompt": "..." }\` form, not mixed into the text. ### A2A Examples #### SendMessage \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-1", "parts": \[ { "text": "Convert https://example.com" } \] } } }' \`\`\` #### SendMessage with file (data URI) \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 2, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-2", "parts": \[ { "text": "{"input":"data:text/plain;base64,SGVsbG8gV29ybGQ="}" } \] } } }' \`\`\` #### SendMessage with structured data \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 3, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-3", "parts": \[ { "data": { "input": "https://example.com", "result": "markdown" }, "mediaType": "application/json" } \] } } }' \`\`\` #### Token activation via A2A \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 7, "method": "SendMessage", "params": { "message": { "messageId": "msg-uuid-7", "parts": \[ { "data": { "input": "https://example.com", "token": "YOUR\_TOKEN", "memo": "YOUR\_PAYMENT\_MEMO" }, "mediaType": "application/json" } \] } } }' \`\`\` > \*\*Note on token activation:\*\* Pass \`token\` and \`memo\` inside a \`data\` Part or as JSON inside a \`text\` Part. A2A does not use HTTP-level Authorization headers. #### Multi-turn conversation (follow-up) \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 4, "method": "SendMessage", "params": { "contextId": "ctx-uuid-1", "message": { "messageId": "msg-uuid-4", "parts": \[ { "text": "Now convert the tables to JSON" } \] } } }' \`\`\` #### GetTask \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 5, "method": "GetTask", "params": { "id": "task\_12345" } }' \`\`\` #### ListTasks \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 6, "method": "ListTasks", "params": { "contextId": "ctx\_12345", "pageSize": 10 } }' \`\`\` ### A2A Message Parts Messages use the A2A \`Part\` format (field-name discriminators per spec v1.0.0): | Type | Description | Fields | | ------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- | | \`text\` | Plain text content or JSON-encoded params | \`text\` | | \`raw\` | File content as base64 bytes; normalized to a data URI (\`data:;base64,\`) for the core | \`raw\` (base64), \`mediaType\` (optional) | | \`data\` | Structured JSON data (core params) | \`data\` (object), \`mediaType\` (optional) | | \`url\` | URL to fetch and convert | \`url\` (http/https) | \*\*Part → Core Parameter Mapping:\*\* - \`text\` Part → \`input\` param (direct content) or JSON-encoded params (\`{ "input": "...", "prompt": "..." }\`) - \`data\` Part → merged as params (\`input\`, \`prompt\`, \`result\`, \`token\`, \`memo\`, etc.) - \`url\` Part → \`input\` param (fetched and converted) ### Message Object \`\`\`typescript interface Message { messageId: string; // REQUIRED: unique ID (e.g. "msg-uuid") contextId?: string; // Optional: group related tasks taskId?: string; // Optional: associate with existing task role: "user" | "agent"; // REQUIRED parts: Array; // REQUIRED: at least one part } \`\`\` ### Task Data Model \`\`\`typescript interface Task { id: string; // "task\_\_" contextId: string; // "ctx\_\_" status: { state: string; // "TASK\_STATE\_WORKING" | "TASK\_STATE\_COMPLETED" | "TASK\_STATE\_FAILED" | "TASK\_STATE\_CANCELED" | "TASK\_STATE\_REJECTED" timestamp: string; // ISO 8601 message?: Message; // only on failure }; artifacts?: Array<{ artifactId: string; name: string; parts: Array; }>; history?: Array; } \`\`\` ### Task States | State | Description | | ---------------------- | ----------------------------- | | \`TASK\_STATE\_WORKING\` | Task is being processed | | \`TASK\_STATE\_COMPLETED\` | Task finished successfully | | \`TASK\_STATE\_FAILED\` | Task failed during processing | | \`TASK\_STATE\_CANCELED\` | Task was canceled by client | | \`TASK\_STATE\_REJECTED\` | Task was rejected by server | ### Error Responses A2A uses JSON-RPC 2.0 error format with A2A-specific error codes: | Code | Error | Description | | -------- | --------------------------------- | ---------------------------------------------------------- | | \`-32700\` | Parse error | Invalid JSON payload | | \`-32600\` | Invalid Request | Missing required fields (message.parts, message.messageId) | | \`-32601\` | Method not found | Unknown A2A method | | \`-32001\` | Task not found | Task ID does not exist | | \`-32002\` | Task is not in a cancelable state | CancelTask on a terminal task | Unsupported media type is returned as HTTP \*\*415\*\* (not a JSON-RPC error code). \*\*Example error response:\*\* \`\`\`json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Invalid Request", "data": \[ { "@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": \[ { "field": "message.messageId", "description": "Message messageId is required" } \] } \] } } \`\`\` ### Streaming Use \`SendStreamingMessage\` for real-time SSE updates: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -H "Accept: text/event-stream" -d '{ "jsonrpc": "2.0", "id": 5, "method": "SendStreamingMessage", "params": { "message": { "messageId": "msg-uuid-5", "parts": \[ { "text": "Convert https://example.com" } \] } } }' \`\`\` Response format (A2A v1.0.0 streaming sequence - exact frames the service emits): \`\`\` data: {"jsonrpc":"2.0","id":5,"result":{"task":{"id":"task\_...","contextId":"ctx\_...","status":{"state":"TASK\_STATE\_WORKING","timestamp":"..."},"artifacts":\[\]}}} data: {"jsonrpc":"2.0","id":5,"result":{"artifactUpdate":{"taskId":"task\_...","contextId":"ctx\_...","artifact":{"artifactId":"artifact\_...","name":"conversion\_result","parts":\[{"text":" partial "}\]},"append":true,"lastChunk":false}}} data: {"jsonrpc":"2.0","id":5,"result":{"artifactUpdate":{"taskId":"task\_...","contextId":"ctx\_...","artifact":{"artifactId":"artifact\_...","name":"conversion\_result","parts":\[{"text":" more "}\]},"append":true,"lastChunk":true}}} data: {"jsonrpc":"2.0","id":5,"result":{"statusUpdate":{"taskId":"task\_...","contextId":"ctx\_...","status":{"state":"TASK\_STATE\_COMPLETED","timestamp":"..."}}}} data: \[DONE\] \`\`\` The first frame carries the full \`task\` in TASK\_STATE\_WORKING; content streams as \`artifactUpdate\` frames (\`lastChunk: true\` on the final chunk); \`statusUpdate\` closes the stream with the terminal TASK\_STATE\_COMPLETED state. The persisted task (via \`GetTask\`) carries the real \`artifacts\[\].parts\[\]\` content. ### Subscribe to Task Subscribe to an existing task for real-time updates: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -H "Accept: text/event-stream" -d '{ "jsonrpc": "2.0", "id": 6, "method": "SubscribeToTask", "params": { "id": "task\_12345" } }' \`\`\` ### Cancel Task Cancel an in-progress task: \`\`\`bash curl -X POST https://mdapi.io/a2a -H "Content-Type: application/a2a+json" -d '{ "jsonrpc": "2.0", "id": 7, "method": "CancelTask", "params": { "id": "task\_12345" } }' \`\`\` Response: \`\`\`json { "jsonrpc": "2.0", "id": 7, "result": { "id": "task\_12345", "contextId": "ctx\_...", "status": { "state": "TASK\_STATE\_CANCELED", "timestamp": "2026-06-26T16:00:00.000Z" } } } \`\`\` ## ACP Configuration Connect mdapi.io to your IDE or coding agent (JetBrains, Cursor, VS Code, etc.) via the Agent Client Protocol v1.0.0. ACP is a JSON-RPC 2.0 endpoint at \`POST /acp\`. Its native surface is \*\*session/turn\*\*: create an ephemeral session, then send a prompt - the converted content streams back as \`session/update\` notifications. > \*\*Single source via \`input\`:\*\* each \`session/prompt\` carries a unified \`input\` - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \[Source Parameters (all protocols)\](#source-parameters-all-protocols). ### Basic Configuration Add to your ACP client configuration (IDE plugin / agent settings): \`\`\`json { "acpServers": { "mdapi": { "url": "https://mdapi.io/acp" } } } \`\`\` Or call the JSON-RPC endpoint directly with \`POST /acp\` (Content-Type: \`application/json\`). Sessions are \*\*stateless and ephemeral\*\* - nothing is persisted server-side, so each \`session/prompt\` runs as an independent conversion: \`\`\`bash # 1. Create a session curl -X POST https://mdapi.io/acp \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":1,"method":"session/new","params":{}}' # 2. Send a prompt (content streams back as session/update notifications) curl -X POST https://mdapi.io/acp \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{ "sessionId":"", "prompt":\[{"type":"text","text":""}, {"type":"resource\_link","uri":"https://example.com"}\] }}' \`\`\` > \*\*Note:\*\* \`GET /acp\` is not supported (ACP is POST-only). A free tier is available without a token. ### ACP Methods | Method | Description | | ---------------- | ------------------------------------------------------------------------------- | | \`initialize\` | Handshake: return protocol version, agent capabilities, and agent info | | \`session/new\` | Create an ephemeral, stateless session (returns a \`sessionId\`) | | \`session/prompt\` | Run a conversion turn; content streams back as \`session/update\` notifications | | \`session/cancel\` | Notification (204, no response body) that best-effort cancels an in-flight turn | ### ACP Session Examples \*\*URL conversion (instructions + \`resource\_link\`):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 1, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[ { "type": "text", "text": "Summarize" }, { "type": "resource\_link", "uri": "https://example.com" } \] } } \`\`\` \*\*Text conversion (a bare \`text\` block becomes the input):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 2, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[{ "type": "text", "text": "Hello World" }\] } } \`\`\` \*\*File conversion (a \`data:\` URI in a \`text\` block is auto-detected as a file):\*\* \`\`\`json { "jsonrpc": "2.0", "id": 3, "method": "session/prompt", "params": { "sessionId": "", "prompt": \[ { "type": "text", "text": "Extract the title" }, { "type": "text", "text": "data:text/plain;base64,SGVsbG8gV29ybGQ=" } \] } } \`\`\` \*\*Token activation (first request only):\*\* every protocol passes the token per-call. Include it on any method's \`params\` as a \`token\` (+ \`memo\` on first use) - ACP does not use a session-level authenticate exchange. ### ACP Response Format Content is delivered \*\*exclusively via \`session/update\` notifications\*\*; the final \`session/prompt\` result carries only the \`stopReason\` (\`PromptResponse\`). A successful turn looks like this SSE stream: \`\`\` data: {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"","update":\[{"content":{"type":"text","text":""},"messageId":""}\]}} data: {"jsonrpc":"2.0","id":1,"result":{"stopReason":"end\_turn"}} data: \[DONE\] \`\`\` - The \`session/update\` \`update\[\]\` array holds the content chunks; \`messageId\` is stable across all chunks of a single turn. - \*\*Streaming:\*\* add \`"stream": true\` to the \`session/prompt\` \`params\` to stream the conversion live - each content chunk arrives as its own \`session/update\` notification (same \`messageId\`), then the \`PromptResponse\`. Without it, the whole turn arrives as one buffered \`session/update\` notification. Streaming is inherent to ACP; content only ever rides notifications. - A failed conversion streams an explanatory \`session/update\`, then ends with \`stopReason: "error"\`. \`session/cancel\` is a client→agent notification - the agent acknowledges it with HTTP 204 and no body. - Token status (\`X-Token-Balance\`, \`X-Token-Expires\`, \`X-Token-Status\`) is proxied from the core into the response headers. ## Usage scenarios mdapi.io is a minimal, self-documenting service-transport primitive: REST, MCP, ACP, A2A, and OpenAI-compatible endpoints all call the same transformation core, so agents can combine protocols and pass already-processed knowledge between each other. - \*\*Agent swarms\*\* - each request is handled by a stateless, automatically-scaled execution environment, so the service scales horizontally. An orchestrator fans work out across a swarm of agents, and the swarm processes very large batches of distinct resources in parallel - millions of resources in a matter of minutes, the ceiling set by how widely the work is distributed rather than by the service. Different users may freely access the same resource. - \*\*Shared vs individual payment\*\* - an orchestrator can pay once for a shared token (batching on-chain activity), or each agent can activate its own token for the exact volume it received. - \*\*Human-in-the-loop\*\* - if an agent has no wallet or insufficient funds, it returns payment details + a QR code; the human pays from a mobile device and the agent resumes. - \*\*Role switching\*\* - an agent's role can change mid-task; one agent fetches/normalizes, another summarizes/extracts, relaying compact results via the text or prompt parameters. - \*\*Cross-protocol interoperability\*\* - knowledge extracted on one protocol is reusable on another. - \*\*Bulk processing / model training\*\* - the swarm pattern turns mdapi.io into a high-throughput edge pipeline for large corpora. See https://mdapi.io/about for the full scenario walkthrough. ## Links - \*\*About service:\*\* https://mdapi.io/about - \*\*API docs:\*\* https://mdapi.io - \*\*MCP server manifest:\*\* https://mdapi.io/mcp - \*\*Health check:\*\* https://mdapi.io/health - \*\*API documentation:\*\* https://mdapi.io/llms.txt - \*\*Full API documentation:\*\* https://mdapi.io/llms-full.txt - \*\*AI discovery:\*\* https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json - \*\*AI Agent discovery:\*\* https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json - \*\*A2A Agent card:\*\* https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json - \*\*ACP manifest:\*\* https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json - \*\*x402 payment manifest:\*\* https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json - \*\*OpenAPI specification (JSON):\*\* https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json - \*\*OpenAPI specification (YAML):\*\* https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml - \*\*MAPI specification (case-insensitive path MAPI.md support):\*\* https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md - \*\*Skill specification (case-insensitive path SKILL.md support):\*\* https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md - \*\*Agent Plugins package (agent-plugins.org v1.0.0):\*\* https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/ ## External Links - \*\*github.com\*\* https://github.com/mdapiio/mdapi.io - \*\*skills.sh\*\* https://www.skills.sh/mdapiio/mdapi.io - \*\*skillsmp.com\*\* https://skillsmp.com/creators/mdapiio/mdapi.io - \*\*clawhub.ai\*\* https://clawhub.ai/mdapiio - \*\*x.com\*\* https://x.com/mdapiio ## Disclaimer \*\*The service is provided "AS IS".\*\* > mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.
이 파일을 서버의 /index.md에 업로드하여 AI 에이전트가 페이지의 깔끔한 버전에 접근할 수 있게 하세요. Accept: text/markdown 콘텐츠 협상을 설정하여 자동으로 제공할 수도 있습니다.
권장 내용
# mdapi.io ## Main - [Home](https://mdapi.io)
전체 llms.txt는 도메인 전체 분석이 필요합니다 (곧 출시)
이 파일을 도메인 루트의 https://mdapi.io/llms.txt에 업로드하세요. ChatGPT, Claude, Perplexity 등의 AI 에이전트가 이 파일을 확인하여 사이트 구조를 파악합니다.
이 사이트에는 이미 llms.txt 파일이 있습니다.
유효한 형식# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems.
Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage.
## Agent entrypoint
- **Start AI discovery** → https://mdapi.io/.well-known/ai-discovery.json
- **Use skill** → https://mdapi.io/.well-known/skill.md
## Quick Start
Choose your entry point based on your role:
| Role | Protocol | Endpoint | When to use |
| ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). |
| AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. |
| AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. |
| OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. |
| Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. |
### Universal discovery
All protocols and capabilities are described in one file:
GET /.well-known/ai-discovery.json
## Features
- Stateless, in-memory processing
- Edge execution with automatic scaling
- Prompt-driven transformation
- AI-optimized output for LLMs
- Pay-per-use via x402 v1/v2 or manual payment
## Supported Formats
| Type | Formats |
| --------- | ------------------------------ |
| Documents | PDF, DOCX, XLSX, XLS, ODT, ODS |
| Images | JPEG, JPG, PNG, WebP, SVG |
| Text | HTML, XML, JSON, CSV, TXT |
| Webpages | Any publicly accessible URL |
## Limits
- Max file size: 50 MB
- Max URL content: 50 MB
- Rate limit: 10,000 requests per hour
- Free tier: 10 requests per day (no token required), within the service’s overall free quota
- Paid tier: min $0.01 per conversion (USDC on Solana)
- Token validity: 1 year
## Authentication
- Bearer: Authorization: Bearer TOKEN
- Header: X-Token-Required: TOKEN
## Token Activation
Before using a paid token, it must be activated:
1. Receive 402 with NEW token+memo (pending created)
2. Send USDC on Solana to wallet with memo from 402
3. Retry request with EXACT token+memo from 402
4. After activation: use token only
Important: The token+memo in 402 response MUST be used - old token or memo will be rejected.
## Method Semantics
| Method | Response Format | Notes |
| ------ | --------------- | ----------------------------------------- |
| GET | Markdown | Always returns Markdown, including errors |
| POST | JSON | Always returns JSON, including errors |
| Error | Same as method | GET errors = Markdown, POST errors = JSON |
## GET / (Conversion)
Parameters: input, prompt, result, stream, token, memo
- input: Auto-detected: starts with "http(s)://" → URL mode (fetched), starts with "data:" → file mode (data URI decoded), otherwise → text mode (processed as-is).
- ⚠️ Browser URL limit: ~2048 chars. Use POST for long text/prompt values.
- prompt: Custom LLM instructions
- result: markdown|prompt|both (default: markdown)
- stream: Enable streaming (boolean, default false) for SSE response
- token: Access token
- memo: Token activation memo
## POST / (JSON body)
Returns JSON with metadata. Accepts JSON body with "input" parameter.
Auto-detection: starts with "http://" → URL, starts with "data:" → file, otherwise → text.
## stream parameter
- true (boolean): Returns SSE stream with real-time chunks
- Token info sent as first message: {"type":"token_info",...}
- Content sent as OpenAI-compatible chunks: {"choices":[{"delta":{"content":"..."},"finish_reason":null}]}
- Final chunk: {"choices":[{"delta":{},"finish_reason":"stop"}]}
- End marker: data: [DONE]
- Native framing per protocol: MCP streams notifications/message chunks, ACP streams session/update notification chunks, A2A streams result.task → artifactUpdate → statusUpdate frames (REST/OpenAI use the choices/delta format above)
## result parameter
- markdown: raw converted Markdown (default)
- prompt: LLM result only (requires prompt)
- both: markdown + prompt_result
## prompt examples
| Use Case | Example |
| ------------------ | ---------------------------------------------------------- |
| Summarize | `?input=https://example.com&prompt=Summarize` |
| Extract key points | `?input=Hello World&prompt=Extract key points` |
| Convert to JSON | `?input=https://example.com&prompt=Convert to JSON format` |
| Analyze content | `?input=Hello World&prompt=Analyze and explain` |
## Response (POST)
{
"success": true,
"markdown": "# Converted content...",
"prompt_result": "... (when result=prompt/both)",
"token_status": "free|valid|invalid|expired|exhausted|expired_pending|activated|verification_error|invalid_payment|error|pending",
"token_balance": 0.99,
"token_expires": 1798761600
}
## Error Codes
| HTTP | code (stable) | Description |
| ---- | -------------------------------------------- | ------------------------------------------------ |
| 200 | - | Success |
| 400 | invalid_request / invalid_input | Bad request |
| 401 | unauthorized / invalid_token / token_expired | Invalid token |
| 402 | payment_required | Payment required |
| 403 | blocked | Blocked |
| 404 | not_found | Not found |
| 413 | too_large / context_overflow | File too large or exceeds the LLM context window |
| 415 | unsupported_format | Format not supported |
| 429 | rate_limited / resource_limit | Rate limit exceeded |
| 500 | server_error | Internal error |
| 503 | ai_unavailable | AI service unavailable |
| 504 | timeout | Timeout |
## Endpoints
| Method | Path | Description |
| ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| GET | /about | About service |
| GET | / | API docs or conversion (via query parameter: `input`, `prompt`, `result`) |
| POST | / | Convert content via JSON body (supports `input`, `prompt`, `result` params) |
| POST | /v1/chat/completions | OpenAI-compatible endpoint |
| GET | /mcp | MCP server manifest |
| POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) |
| POST | /acp | ACP RPC endpoint (IDE agents) |
| POST | /a2a | A2A RPC endpoint (agent2agent) |
| GET | /health | Health check |
| GET | /llms.txt | API documentation |
| GET | /llms-full.txt | Full API documentation |
| GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery |
| GET | /.well-known/agent.json or /agent.json | AI Agent discovery |
| GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card |
| GET | /.well-known/acp.json or /acp.json | ACP manifest |
| GET | /.well-known/x402.json or /x402.json | x402 payment manifest |
| GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) |
| GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) |
| GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) |
| GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) |
| GET | /.well-known/skills/index.json | Legacy skills index |
| GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) |
| GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) |
| GET | /.well-known/mcp/server-cards.json | MCP server cards index |
| GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest |
| GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config |
| GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill |
## Links
- **About service:** https://mdapi.io/about
- **API docs:** https://mdapi.io
- **MCP server manifest:** https://mdapi.io/mcp
- **Health check:** https://mdapi.io/health
- **API documentation:** https://mdapi.io/llms.txt
- **Full API documentation:** https://mdapi.io/llms-full.txt
- **AI discovery:** https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json
- **AI Agent discovery:** https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json
- **A2A Agent card:** https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json
- **ACP manifest:** https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json
- **x402 payment manifest:** https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json
- **OpenAPI specification (JSON):** https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json
- **OpenAPI specification (YAML):** https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml
- **MAPI specification (case-insensitive path MAPI.md support):** https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md
- **Skill specification (case-insensitive path SKILL.md support):** https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md
- **Agent Plugins package (agent-plugins.org v1.0.0):** https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/
## External Links
- **github.com** https://github.com/mdapiio/mdapi.io
- **skills.sh** https://www.skills.sh/mdapiio/mdapi.io
- **skillsmp.com** https://skillsmp.com/creators/mdapiio/mdapi.io
- **clawhub.ai** https://clawhub.ai/mdapiio
- **x.com** https://x.com/mdapiio
## Disclaimer
**The service is provided "AS IS".**
> mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.
접근성
Content available without JavaScript
Main content starts at 100% of HTML
Page size: 58KB
AI 발견 가능성
5/5 AI search bots blocked: OAI-SearchBot, Claude-SearchBot, PerplexityBot, DuckAssistBot, Amazonbot
Sitemap found
robots.txt exists
llms.txt exists and is valid
구조화 데이터
No JSON-LD / Schema.org found
0/3 OG tags present
No meta description
No canonical URL
No lang attribute on <html>
시맨틱 HTML
No headings found
Missing <article> and <main> elements
No divs or semantic elements found
No images found
No divs found
콘텐츠 효율성
0% token reduction (HTML→Markdown)
Content ratio: 99.5% (59274 content chars / 59583 HTML bytes)
HTML size: 58KB
0/28 elements with inline styles (0.0%)
{
"url": "https://mdapi.io",
"timestamp": 1788436399512,
"fetch": {
"mode": "simple",
"timeMs": 851,
"htmlSizeBytes": 59583,
"supportsMarkdown": true,
"markdownAgents": {
"contentNegotiation": true,
"mdUrl": {
"found": false,
"url": null
},
"linkTag": {
"found": false,
"url": null
},
"linkHeader": {
"found": false,
"url": null
},
"responseHeaders": {
"contentSignal": null,
"xMarkdownTokens": null,
"vary": null
},
"frontmatter": {
"present": false,
"fields": [],
"level": "none"
},
"level": "cloudflare",
"contentNegotiationMediaType": "text/markdown",
"properMediaType": true
},
"statusCode": 200
},
"extraction": {
"title": "",
"excerpt": "",
"byline": null,
"siteName": null,
"lang": null,
"contentLength": 59274,
"metadata": {
"description": null,
"ogTitle": null,
"ogDescription": null,
"ogImage": null,
"ogType": null,
"canonical": null,
"lang": null,
"schemas": [],
"robotsMeta": null,
"author": null,
"generator": null,
"markdownAlternateHref": null
}
},
"markdown": "\\# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems. Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage. ## Agent entrypoint - \\*\\*Start AI discovery\\*\\* → https://mdapi.io/.well-known/ai-discovery.json - \\*\\*Use skill\\*\\* → https://mdapi.io/.well-known/skill.md ## Quick Start Choose your entry point based on your role: | Role | Protocol | Endpoint | When to use | | ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). | | AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. | | AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. | | OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. | | Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. | ### Universal discovery All protocols and capabilities are described in one file: GET /.well-known/ai-discovery.json ## Features - Stateless, in-memory processing - Edge execution with automatic scaling - Prompt-driven transformation - AI-optimized output for LLMs - Pay-per-use via x402 v1/v2 or manual payment ## Supported Formats | Type | Formats | | --------- | ------------------------------ | | Documents | PDF, DOCX, XLSX, XLS, ODT, ODS | | Images | JPEG, JPG, PNG, WebP, SVG | | Text | HTML, XML, JSON, CSV, TXT | | Webpages | Any publicly accessible URL | ## Source Parameters (all protocols) Every protocol (REST, MCP, ACP, A2A, OpenAI) converges on the \\*\\*same conversion core\\*\\*, so content is specified via a single unified \\`input\\` parameter everywhere. | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | \\`input\\` | string | Content to convert: a URL (http(s)://...), a data URI (data:mime/type;base64,...), or plain text. Auto-detected: http(s):// → URL, data: → file, otherwise → text. | The \\`input\\` parameter is auto-detected by the core: URLs (starting with \\`http://\\` or \\`https://\\`) are fetched, data URIs (starting with \\`data:\\`) are decoded as files, and anything else is treated as raw text. Additional parameters (\\`prompt\\`, \\`result\\`, \\`stream\\`, \\`token\\`, \\`memo\\`) are orthogonal and may be combined with \\`input\\`. All five protocols expose the same \\`input\\` source and apply the same transformations, streaming, and prompt-driven processing - the only difference is the transport (REST query/JSON, MCP \\`tools/call\\`, ACP \\`session/prompt\\`, OpenAI \\`messages\\`, A2A \\`message.parts\\`). ## Limits | Limit | Value | | ------------------- | -------------------------------------------------------------------------------- | | \\*\\*Max file size\\*\\* | 50 MB | | \\*\\*Max URL content\\*\\* | 50 MB | | \\*\\*Rate limit\\*\\* | 10,000 requests per hour | | \\*\\*Free tier\\*\\* | 10 requests per day (no token required), within the service’s overall free quota | | \\*\\*Paid tier\\*\\* | min $0.01 per conversion (USDC on Solana) | | \\*\\*Token validity\\*\\* | 1 year | ## Authentication \\*\\*Recommended:\\*\\* Use \\`Authorization: Bearer TOKEN\\` | Method | Use Case | | -------------------- | ---------------------------------- | | Bearer (recommended) | \\`-H \"Authorization: Bearer TOKEN\"\\` | | Header | \\`-H \"X-Token-Required: TOKEN\"\\` | ## Token Activation Before you can use a paid token, you must receive a 402 response first: | Step | Description | | ---- | ------------------------------------------------------- | | 1 | Request without token → Receive 402 with NEW token+memo | | 2 | Send USDC on Solana to wallet with memo from 402 | | 3 | Retry with EXACT token+memo from 402 → Activation | | 4 | After: use token only (no memo needed) | Important: The token+memo issued in the 402 response MUST be used exactly. Using old token or different memo will be rejected. ## API Usage ### GET / (Content conversion) Simple content conversion using query parameters. Returns Markdown directly. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \\`input\\` | string | \\* | Content to convert (URL, text, or data URI - auto-detected) | | \\`prompt\\` | string | | Custom instructions for LLM processing | | \\`result\\` | string | | Response format: \\`markdown\\`, \\`prompt\\`, or \\`both\\` | | \\`stream\\` | boolean | | Enable streaming: true for SSE response | | \\`token\\` | string | | Access token for paid tier | | \\`memo\\` | string | | Memo for token activation | \\*The \\`input\\` parameter is required.\\* > \\*\\*⚠️ Browser URL limit:\\*\\* GET requests with long \\`input\\` or \\`prompt\\` values may exceed browser URL limits (~2048 characters). Use POST with JSON body for large payloads. ### POST / (Content conversion via JSON) Supports content conversion via JSON body. The \\`input\\` parameter accepts URLs, text, or data URIs (auto-detected). Returns a JSON object containing the Markdown content. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \\`input\\` | string | \\* | Content to convert (URL, text, or data URI - auto-detected) | | \\`prompt\\` | string | | Custom instructions for LLM processing | | \\`result\\` | string | | Response format: \\`markdown\\`, \\`prompt\\`, or \\`both\\` | | \\`stream\\` | boolean | | Enable streaming: true for SSE response | | \\`token\\` | string | | Access token for paid tier | | \\`memo\\` | string | | Memo for token activation | \\*The \\`input\\` parameter is required.\\* ### Result Format Parameter The \\`result\\` parameter controls the response format for both GET and POST requests. | Value | Description | | -------------------- | --------------------------------------------------------------- | | \\`markdown\\` (default) | Returns the converted Markdown content | | \\`prompt\\` | Returns the result of LLM processing with \\`prompt\\` instructions | | \\`both\\` | Returns both \\`markdown\\` and \\`prompt\\_result\\` in the response | When \\`result=both\\`: - \\*\\*GET requests\\*\\* return Markdown combining \\`markdown\\`, followed by \"## Prompt Result\" and \\`prompt\\_result\\` (always in Markdown format) - \\*\\*POST requests\\*\\* return JSON with \\`markdown\\` and \\`prompt\\_result\\` fields > \\*\\*Auto \\`result\\`:\\*\\* When \\`prompt\\` is provided without an explicit \\`result\\`, the core automatically sets \\`result=\"prompt\"\\` (LLM output only). Without \\`prompt\\`, default is \\`result=\"markdown\"\\`. Only specify \\`result\\` explicitly when you need both (\\`result=\"both\"\\`). ### Prompt Parameter The \\`prompt\\` parameter lets you specify custom instructions for the LLM to follow when generating the result. | Use Case | Example | | ------------------ | ---------------------------------------------------------- | | Summarize | \\`?input=https://example.com&prompt=Summarize\\` | | Extract key points | \\`?input=Hello World&prompt=Extract key points\\` | | Convert to JSON | \\`?input=https://example.com&prompt=Convert to JSON format\\` | | Analyze content | \\`?input=Hello World&prompt=Analyze and explain\\` | ### Streaming Parameter The \\`stream\\` parameter enables Server-Sent Events (SSE) streaming for real-time response delivery. \\*\\*Type:\\*\\* \\`boolean\\` \\*\\*Default:\\*\\* \\`false\\` (non-streaming) Example: \\`\\`\\`bash curl \"https://mdapi.io/?input=...&stream=true\" \\`\\`\\` Response format (OpenAI-compatible SSE, one JSON object per \\`data:\\` line): \\`\\`\\`json data: {\"type\":\"token\\_info\",\"token\\_status\":\"valid\",\"token\\_balance\":0.99,\"token\\_expires\":1798761600} data: {\"choices\":\\[{\"index\":0,\"delta\":{\"content\":\" chunk\"},\"finish\\_reason\":null}\\]} data: {\"choices\":\\[{\"index\":0,\"delta\":{\"content\":\" more\"},\"finish\\_reason\":null}\\]} data: {\"choices\":\\[{\"index\":0,\"delta\":{},\"finish\\_reason\":\"stop\"}\\]} data: \\[DONE\\] \\`\\`\\` \\*\\*Native streaming per protocol.\\*\\* Every protocol delivers a \\*real\\* content stream when \\`stream: true\\`, but each emits it in its own native frame format (so existing clients keep working): | Protocol | Streaming frame format | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | REST | OpenAI-compatible \\`choices/delta\\` frames | | OpenAI | \\`chat.completion.chunk\\` (\\`choices/delta\\`) | | MCP | \\`notifications/message\\` content chunks, then one final \\`tools/call\\` result frame | | ACP | \\`session/update\\` notification chunks (one stable \\`messageId\\` per turn), then a final response carrying only \\`stopReason\\` | | A2A | \\`result.task\\` (\\`TASK\\_STATE\\_WORKING\\`) start frame, \\`result.artifactUpdate\\` (\\`{artifact, append, lastChunk}\\`) content frames, then \\`result.statusUpdate\\` (\\`TASK\\_STATE\\_COMPLETED\\`) - stream closes | > \\*\\*Note on MCP transport vs. the \\`stream\\` parameter.\\*\\* The MCP manifest advertises \\`transport.type: \"streamable-http\"\\` - that is the MCP \\*transport\\* > (how JSON-RPC requests are delivered to \\`POST /mcp\\`). It is unrelated to the \\`stream\\` \\*parameter\\*, which independently enables SSE streaming of the > conversion \\*\\*content\\*\\*. You can use MCP without streaming; and when you do pass \\`stream: true\\`, the content arrives as SSE frames alongside the transport. ### Response Codes | Code | Description | Response Body (GET) | Response Body (POST) | | ---- | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | | 200 | Success | Markdown content | JSON with \\`success\\`, markdown, prompt\\_result, metrics, token fields | | 402 | Payment Required | Markdown payment instructions (\\`X-Error-Code: payment\\_required\\`) | JSON with \\`success:false\\`, \\`code:\"payment\\_required\"\\`, and payment object | | 400 | Bad Request | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"invalid\\_request\"}\\` | | 401 | Invalid Token | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"unauthorized\"}\\` | | 404 | Not Found | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"not\\_found\"}\\` | | 413 | Payload Too Large | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"too\\_large\"}\\` | | 429 | Rate Limited | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"rate\\_limited\"}\\` | | 500 | Server Error | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"server\\_error\"}\\` | ### Token Status The X-Token-Status header (and token\\_status field in responses) indicates the current state of authentication: | Status | Description | | ------------------ | --------------------------------------------------------------------------------------- | | free | Free tier (no token required, 10 requests/day), within the service's overall free quota | | valid | Paid token active with remaining balance | | invalid | Token not found or not provided | | expired | Token validity period has ended | | exhausted | Token balance has been fully used | | expired\\_pending | Activation memo has expired | | activated | Token was just activated with this request | | verification\\_error | Payment verification failed | | invalid\\_payment | Payment transaction is invalid | | error | Internal error during token processing | | pending | Payment required (token not yet activated) | ### Endpoints | Method | Path | Description | | ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | GET | /about | About service | | GET | / | API docs or conversion (via query parameter: \\`input\\`, \\`prompt\\`, \\`result\\`) | | POST | / | Convert content via JSON body (supports \\`input\\`, \\`prompt\\`, \\`result\\` params) | | POST | /v1/chat/completions | OpenAI-compatible endpoint | | GET | /mcp | MCP server manifest | | POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) | | POST | /acp | ACP RPC endpoint (IDE agents) | | POST | /a2a | A2A RPC endpoint (agent2agent) | | GET | /health | Health check | | GET | /llms.txt | API documentation | | GET | /llms-full.txt | Full API documentation | | GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery | | GET | /.well-known/agent.json or /agent.json | AI Agent discovery | | GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card | | GET | /.well-known/acp.json or /acp.json | ACP manifest | | GET | /.well-known/x402.json or /x402.json | x402 payment manifest | | GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) | | GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) | | GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) | | GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) | | GET | /.well-known/skills/index.json | Legacy skills index | | GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) | | GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) | | GET | /.well-known/mcp/server-cards.json | MCP server cards index | | GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest | | GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config | | GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill | #### Examples \\`\\`\\`bash # URL conversion via GET (free) curl \"https://mdapi.io/?input=https://example.com\" # URL with prompt and result=both (returns markdown + prompt\\_result) curl \"https://mdapi.io/?input=https://example.com&prompt=Summarize&result=both\" # Text with prompt (auto result=prompt) curl \"https://mdapi.io/?input=Hello World&prompt=Summarize\" # Token activation via GET (activate and use) curl -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" \"https://mdapi.io/?input=https://example.com\" # Paid request with token via GET (using token) curl -H \"Authorization: Bearer YOUR\\_TOKEN\" \"https://mdapi.io/?input=https://example.com\" # URL conversion via POST (free) curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" # Text with prompt via POST curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"Hello World\",\"prompt\":\"Summarize\",\"result\":\"both\"}' \"https://mdapi.io/\" # File upload via POST (data URI) curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"data:text/plain;base64,SGVsbG8gV29ybGQ=\"}' \"https://mdapi.io/\" # Token activation via POST curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" # Paid request with token via POST curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" \\`\\`\\` ### OpenAI Compatible Endpoint The \\`/v1/chat/completions\\` endpoint provides an OpenAI‑compatible API for markdown conversion with streaming support. \\*\\*Supported features:\\*\\* - URL extraction from message content (any text containing https?://) - image\\_url in messages (OpenAI format) - supports HTTP URLs and data URLs - file in messages (OpenAI format) - base64 encoded files (field \\`file.data\\`, optional \\`file.mimeType\\`; built into a \\`data:\\` URI for the core - no \\`file.filename\\` required or used) - Direct text content in messages (any text without a URL is sent to the core as the \\`input\\` source and converted to Markdown) - Token and memo via headers (recommended for POST) - Streaming SSE responses (\\`stream: true\\`) - Custom instructions with LLM processing (system messages, or user messages containing instruction keywords such as \\*extract, summarize, analyze, format, convert to, write as, create, generate, json\\* → LLM-driven summary/extraction/transformation) - \\`prompt\\` for LLM-processed output. The response surfaces \\`prompt\\_result\\` at the top level alongside the standard \\`choices\\[\\].message.content\\` (which carries \\`prompt\\_result\\` when prompt is set, otherwise the Markdown). \\`model\\` is accepted but not required (any string; the service uses its own conversion pipeline, not a remote LLM chat model, unless custom instructions trigger LLM processing). > \\*\\*Content via message text:\\*\\* the message text is passed to the core as the \\`input\\` parameter - the same unified source as every other protocol. URLs are auto-detected, data URIs are decoded as files, and plain text is processed directly. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). #### Request Schema \\`\\`\\`json { \"type\": \"object\", \"properties\": { \"model\": { \"type\": \"string\", \"description\": \"Optional model identifier (any string accepted; not required)\" }, \"messages\": { \"type\": \"array\", \"items\": { \"type\": \"object\", \"properties\": { \"role\": {\"type\": \"string\", \"enum\": \\[\"user\", \"system\", \"assistant\"\\]}, \"content\": {\"oneOf\": \\[{\"type\": \"string\"}, {\"type\": \"array\"}\\]} } }, \"description\": \"Chat messages. URL in content, image\\_url or file in content for input\" }, \"stream\": { \"type\": \"boolean\", \"default\": false, \"description\": \"Enable streaming SSE responses\" }, \"prompt\": { \"type\": \"string\", \"description\": \"Custom instructions for LLM processing (alternative to instruction keywords in messages)\" }, \"result\": { \"type\": \"string\", \"enum\": \\[\"markdown\", \"prompt\", \"both\"\\], \"description\": \"Response format when using prompt\" }, \"token\": {\"type\": \"string\", \"description\": \"Access token for paid tier\"}, \"memo\": {\"type\": \"string\", \"description\": \"Memo for token activation\"}, \"input\": { \"type\": \"string\", \"description\": \"Content to convert (URL, text, or data URI - auto-detected). Alternative to a URL/file embedded in messages\" } }, \"required\": \\[\"messages\"\\] } \\`\\`\\` > \\*\\*Note on streaming + custom instructions:\\*\\* when custom instructions trigger LLM processing, the response is returned as a single completion (streaming is not applied to the LLM pass). Streaming SSE applies to the standard conversion path. ## MCP Configuration Connect mdapi.io to your MCP-compatible client (spec 2026-07-28, stateless). > \\*\\*Single source via \\`input\\`:\\*\\* the \\`convert\\` tool accepts a unified \\`input\\` parameter - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). ### Protocol Requirements - \\*\\*Transport:\\*\\* Streamable HTTP (POST-only for JSON-RPC, GET for manifest) - \\*\\*Required headers:\\*\\* \\`MCP-Protocol-Version: 2026-07-28\\` and \\`Mcp-Method\\` on every request; \\`Mcp-Name\\` additionally on \\`tools/call\\`, \\`resources/read\\`, and \\`prompts/get\\` - \\*\\*Stateless:\\*\\* No sessions - every request is independent - \\*\\*Discovery:\\*\\* Use \\`server/discover\\` to query server capabilities and supported versions ### Basic Configuration Add to your MCP config file: \\`\\`\\`json { \"mcpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/mcp\" } } } \\`\\`\\` > \\*\\*Note:\\*\\* No token is required to connect. A free tier is available (10 requests per day), within the service’s overall free quota. ### OpenClaw Integration OpenClaw can use mdapi.io in two ways: \\*\\*Option 1: Via MCP (Recommended)\\*\\* \\`\\`\\`json { \"mcpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/mcp\" } } } \\`\\`\\` \\*\\*Option 2: Via OpenAI-compatible endpoint\\*\\* \\`\\`\\`bash openclaw config set llm.apiBase https://mdapi.io/v1 openclaw config set llm.apiKey YOUR\\_TOKEN \\`\\`\\` ### Using MCP with a token MCP does not use HTTP-level Authorization headers. The token is always passed inside the tool \\`arguments\\` object. \\*\\*Activation\\*\\* - include \\`token\\` + \\`memo\\` in the first request: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_TOKEN\", \"memo\": \"YOUR\\_PAYMENT\\_MEMO\" } } } \\`\\`\\` \\*\\*After activation\\*\\* - use \\`token\\` only (no memo needed): \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_ACTIVATED\\_TOKEN\" } } } \\`\\`\\` ### MCP Tool Examples Convert with prompt and result: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"prompt\": \"Summarize\", \"result\": \"both\", \"token\": \"YOUR\\_TOKEN\" } } } \\`\\`\\` Process text directly: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"Hello World\", \"prompt\": \"Extract key points\", \"result\": \"prompt\" } } } \\`\\`\\` Stream with SSE (native MCP frames): \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"stream\": true } } } \\`\\`\\` Response (SSE over \\`streamable-http\\`): intermediate \\`notifications/message\\` content chunks, then one final \\`tools/call\\` result frame with the full Markdown: \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\",\"data\":\" partial \"}} data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\",\"data\":\" more \"}} data: {\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"content\":\\[{\"type\":\"text\",\"text\":\"\"}\\],\"isError\":false}} data: \\[DONE\\] \\`\\`\\` ### Using Environment Variables Or use environment variable: \\`\\`\\`bash export MDAPI\\_TOKEN=YOUR\\_ACTIVATED\\_TOKEN \\`\\`\\` ## Code Examples ### JavaScript (fetch) \\`\\`\\`javascript // Convert a URL via GET - returns Markdown directly const response = await fetch('https://mdapi.io/?input=https://example.com'); const markdown = await response.text(); console.log(markdown); \\`\\`\\` \\`\\`\\`javascript // Convert a URL via POST - returns JSON with metadata const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); console.log(data.markdown); \\`\\`\\` \\`\\`\\`javascript // Text with prompt - returns prompt\\_result const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'Hello World', prompt: 'Summarize', result: 'both' }) }); const data = await response.json(); console.log(data.markdown); console.log(data.prompt\\_result); \\`\\`\\` \\`\\`\\`javascript // File upload via data URI const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'data:text/plain;base64,SGVsbG8gV29ybGQ=' }) }); const data = await response.json(); console.log(data.markdown); \\`\\`\\` \\`\\`\\`javascript // Token activation - first request with token + memo const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\\_TOKEN', 'X-Memo-Required': 'YOUR\\_MEMO' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); // After activation, use token only (no memo needed) \\`\\`\\` \\`\\`\\`javascript // Streaming via OpenAI-compatible endpoint const response = await fetch('https://mdapi.io/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\\_TOKEN' }, body: JSON.stringify({ model: 'markdown-v1', messages: \\[{ role: 'user', content: 'Convert https://example.com' }\\], stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; process.stdout.write(decoder.decode(value)); } \\`\\`\\` ### Python \\`\\`\\`python import requests # Convert a URL via GET - returns Markdown directly response = requests.get('https://mdapi.io/?input=https://example.com') response.raise\\_for\\_status() print(response.text) \\`\\`\\` \\`\\`\\`python import requests # Convert a URL via POST - returns JSON with metadata response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) \\`\\`\\` \\`\\`\\`python import requests # Text with prompt - returns prompt\\_result response = requests.post( 'https://mdapi.io/', json={'input': 'Hello World', 'prompt': 'Summarize', 'result': 'both'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) print(data\\['prompt\\_result'\\]) \\`\\`\\` \\`\\`\\`python import requests # File upload via data URI with open('document.pdf', 'rb') as f: import base64 file\\_data = base64.b64encode(f.read()).decode() response = requests.post( 'https://mdapi.io/', json={'input': f'data:text/plain;base64,{file\\_data}'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) \\`\\`\\` \\`\\`\\`python import requests # Token activation response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'}, headers={ 'Authorization': 'Bearer YOUR\\_TOKEN', 'X-Memo-Required': 'YOUR\\_MEMO' } ) response.raise\\_for\\_status() # After activation, use token only (no memo needed) \\`\\`\\` \\`\\`\\`python from openai import OpenAI # OpenAI-compatible streaming client = OpenAI(base\\_url='https://mdapi.io/v1', api\\_key='YOUR\\_TOKEN') stream = client.chat.completions.create( model='markdown-v1', messages=\\[{'role': 'user', 'content': 'Convert https://example.com'}\\], stream=True ) for chunk in stream: if chunk.choices\\[0\\].delta.content: print(chunk.choices\\[0\\].delta.content, end='') \\`\\`\\` ### Go \\`\\`\\`go package main import ( \"fmt\" \"io\" \"net/http\" ) func main() { // Convert a URL via GET - returns Markdown directly resp, err := http.Get(\"https://mdapi.io/?input=https://example.com\") if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() body, \\_ := io.ReadAll(resp.Body) fmt.Println(string(body)) } \\`\\`\\` \\`\\`\\`go package main import ( \"bytes\" \"encoding/json\" \"fmt\" \"net/http\" ) func main() { // Convert a URL via POST - returns JSON with metadata payload, \\_ := json.Marshal(map\\[string\\]string{ \"input\": \"https://example.com\", }) resp, err := http.Post( \"https://mdapi.io/\", \"application/json\", bytes.NewReader(payload), ) if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() var result map\\[string\\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\\[\"markdown\"\\]) } \\`\\`\\` \\`\\`\\`go package main import ( \"bytes\" \"encoding/base64\" \"encoding/json\" \"fmt\" \"net/http\" \"os\" ) func main() { // File upload via data URI fileBytes, \\_ := os.ReadFile(\"document.pdf\") b64 := base64.StdEncoding.EncodeToString(fileBytes) payload, \\_ := json.Marshal(map\\[string\\]string{ \"input\": \"data:text/plain;base64,\" + b64, }) resp, err := http.Post( \"https://mdapi.io/\", \"application/json\", bytes.NewReader(payload), ) if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() var result map\\[string\\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\\[\"markdown\"\\]) } \\`\\`\\` ### Rust \\`\\`\\`rust use anyhow::Result; use reqwest::Client; #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let url = \"https://mdapi.io/?input=https://example.com\"; let response = client.get(url).send().await?; response.error\\_for\\_status\\_ref()?; let markdown = response.text().await?; println!(\"{}\", markdown); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::{Result, Context}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #\\[derive(Deserialize)\\] struct ApiResponse { markdown: Option, prompt\\_result: Option, } #\\[derive(Serialize)\\] struct ConvertRequest { input: String, #\\[serde(skip\\_serializing\\_if = \"Option::is\\_none\")\\] prompt: Option, #\\[serde(skip\\_serializing\\_if = \"Option::is\\_none\")\\] result: Option, } pub struct MdApiClient { client: Client, base\\_url: String, token: Option, } impl MdApiClient { pub fn new(token: Option) -> Self { Self { client: Client::new(), base\\_url: \"https://mdapi.io\".to\\_string(), token, } } pub async fn convert\\_url( &self, url: &str, prompt: Option<&str>, ) -> Result { let body = ConvertRequest { input: url.to\\_string(), prompt: prompt.map(|p| p.to\\_string()), result: prompt.map(|\\_| \"both\".to\\_string()), }; let mut request = self.client .post(&self.base\\_url) .header(\"Content-Type\", \"application/json\") .json(&body); if let Some(ref token) = self.token { request = request.header(\"Authorization\", format!(\"Bearer {}\", token)); } let response = request .send() .await .context(\"Failed to send HTTP request\")?; response .error\\_for\\_status\\_ref() .context(\"API returned error status\")?; response .text() .await .context(\"Failed to read response body\") } } // Example: convert a URL with prompt #\\[tokio::main\\] async fn main() -> Result<()> { let token = std::env::var(\"MDAPI\\_TOKEN\").ok(); let client = MdApiClient::new(token); let markdown = client .convert\\_url(\"https://example.com\", Some(\"Summarize\")) .await?; println!(\"{}\", markdown); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::Result; use reqwest::Client; use base64::engine::general\\_purpose::STANDARD; use base64::Engine; #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let file\\_bytes = std::fs::read(\"document.pdf\")?; let b64 = STANDARD.encode(&file\\_bytes); let body = serde\\_json::json!({ \"input\": format!(\"data:text/plain;base64,{}\", b64) }); let response = client .post(\"https://mdapi.io/\") .header(\"Content-Type\", \"application/json\") .json(&body) .send() .await?; let data: serde\\_json::Value = response.json().await?; println!(\"{}\", data\\[\"markdown\"\\]); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::Result; use futures\\_util::stream::StreamExt; use reqwest::Client; // Streaming response when using the OpenAI‑compatible endpoint with stream = true #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let token = std::env::var(\"MDAPI\\_TOKEN\")?; let body = serde\\_json::json!({ \"model\": \"markdown-v1\", \"messages\": \\[ { \"role\": \"user\", \"content\": \"Convert https://example.com\" } \\], \"stream\": true }); let response = client .post(\"https://mdapi.io/v1/chat/completions\") .header(\"Authorization\", format!(\"Bearer {}\", token)) .json(&body) .send() .await?; response.error\\_for\\_status\\_ref()?; let mut stream = response.bytes\\_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; let text = String::from\\_utf8\\_lossy(&chunk); eprint!(\"{}\", text); } Ok(()) } \\`\\`\\` ### OpenAI SDK \\`\\`\\`python from openai import OpenAI client = OpenAI( base\\_url=\"https://mdapi.io/v1\", api\\_key=\"YOUR\\_TOKEN\" ) try: response = client.chat.completions.create( model=\"markdown-v1\", messages=\\[{\"role\": \"user\", \"content\": \"Convert https://example.com\"}\\] ) print(response.choices\\[0\\].message.content) except Exception as e: print(\"API error:\", e) \\`\\`\\` #### OpenAI with paid token \\`\\`\\`bash curl -X POST \"https://mdapi.io/v1/chat/completions\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" -H \"Content-Type: application/json\" -d '{\"model\":\"markdown-v1\",\"messages\":\\[{\"role\":\"user\",\"content\":\"Convert https://example.com\"}\\]}' \\`\\`\\` After activation, use token only (no memo needed): \\`\\`\\`bash curl -X POST \"https://mdapi.io/v1/chat/completions\" -H \"Authorization: Bearer YOUR\\_ACTIVATED\\_TOKEN\" -H \"Content-Type: application/json\" -d '{\"model\":\"markdown-v1\",\"messages\":\\[{\"role\":\"user\",\"content\":\"Convert https://example.com\"}\\]}' \\`\\`\\` ## A2A Configuration Connect mdapi.io to your A2A-compatible agent (Claude Code, Codex, OpenClaw, Hermes, etc.). ### Basic Configuration Add to your A2A client configuration: \\`\\`\\`json { \"agent\": { \"name\": \"mdapi\", \"agentCard\": { \"url\": \"https://mdapi.io/.well-known/agent-card.json\" } } } \\`\\`\\` Or use JSON-RPC directly: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-1\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } } \\`\\`\\` ### A2A Methods | Method | Description | | -------------------- | --------------------------------------- | | SendMessage | Send a message to initiate conversion | | SendStreamingMessage | Send message with SSE streaming updates | | GetTask | Get task status and results by ID | | ListTasks | List tasks with optional filtering | | CancelTask | Cancel an in-progress task | | SubscribeToTask | Subscribe to task updates via SSE | > \\*\\*Single source via \\`input\\`:\\*\\* the \\`input\\` parameter in the message parts is the unified source - the same as the REST endpoint. A bare URL > inside a text part (e.g. \\`\"Convert https://example.com\"\\`) is extracted automatically and used as the conversion source, so you don't need to wrap it > in structured JSON. Instructions such as \\`Summarize\\` should be passed via the structured \\`{ \"input\": \"...\", \"prompt\": \"...\" }\\` form, not mixed into the text. ### A2A Examples #### SendMessage \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-1\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } }' \\`\\`\\` #### SendMessage with file (data URI) \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-2\", \"parts\": \\[ { \"text\": \"{\"input\":\"data:text/plain;base64,SGVsbG8gV29ybGQ=\"}\" } \\] } } }' \\`\\`\\` #### SendMessage with structured data \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-3\", \"parts\": \\[ { \"data\": { \"input\": \"https://example.com\", \"result\": \"markdown\" }, \"mediaType\": \"application/json\" } \\] } } }' \\`\\`\\` #### Token activation via A2A \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 7, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-7\", \"parts\": \\[ { \"data\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_TOKEN\", \"memo\": \"YOUR\\_PAYMENT\\_MEMO\" }, \"mediaType\": \"application/json\" } \\] } } }' \\`\\`\\` > \\*\\*Note on token activation:\\*\\* Pass \\`token\\` and \\`memo\\` inside a \\`data\\` Part or as JSON inside a \\`text\\` Part. A2A does not use HTTP-level Authorization headers. #### Multi-turn conversation (follow-up) \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 4, \"method\": \"SendMessage\", \"params\": { \"contextId\": \"ctx-uuid-1\", \"message\": { \"messageId\": \"msg-uuid-4\", \"parts\": \\[ { \"text\": \"Now convert the tables to JSON\" } \\] } } }' \\`\\`\\` #### GetTask \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 5, \"method\": \"GetTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` #### ListTasks \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 6, \"method\": \"ListTasks\", \"params\": { \"contextId\": \"ctx\\_12345\", \"pageSize\": 10 } }' \\`\\`\\` ### A2A Message Parts Messages use the A2A \\`Part\\` format (field-name discriminators per spec v1.0.0): | Type | Description | Fields | | ------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- | | \\`text\\` | Plain text content or JSON-encoded params | \\`text\\` | | \\`raw\\` | File content as base64 bytes; normalized to a data URI (\\`data:;base64,\\`) for the core | \\`raw\\` (base64), \\`mediaType\\` (optional) | | \\`data\\` | Structured JSON data (core params) | \\`data\\` (object), \\`mediaType\\` (optional) | | \\`url\\` | URL to fetch and convert | \\`url\\` (http/https) | \\*\\*Part → Core Parameter Mapping:\\*\\* - \\`text\\` Part → \\`input\\` param (direct content) or JSON-encoded params (\\`{ \"input\": \"...\", \"prompt\": \"...\" }\\`) - \\`data\\` Part → merged as params (\\`input\\`, \\`prompt\\`, \\`result\\`, \\`token\\`, \\`memo\\`, etc.) - \\`url\\` Part → \\`input\\` param (fetched and converted) ### Message Object \\`\\`\\`typescript interface Message { messageId: string; // REQUIRED: unique ID (e.g. \"msg-uuid\") contextId?: string; // Optional: group related tasks taskId?: string; // Optional: associate with existing task role: \"user\" | \"agent\"; // REQUIRED parts: Array; // REQUIRED: at least one part } \\`\\`\\` ### Task Data Model \\`\\`\\`typescript interface Task { id: string; // \"task\\_\\_\" contextId: string; // \"ctx\\_\\_\" status: { state: string; // \"TASK\\_STATE\\_WORKING\" | \"TASK\\_STATE\\_COMPLETED\" | \"TASK\\_STATE\\_FAILED\" | \"TASK\\_STATE\\_CANCELED\" | \"TASK\\_STATE\\_REJECTED\" timestamp: string; // ISO 8601 message?: Message; // only on failure }; artifacts?: Array<{ artifactId: string; name: string; parts: Array; }>; history?: Array; } \\`\\`\\` ### Task States | State | Description | | ---------------------- | ----------------------------- | | \\`TASK\\_STATE\\_WORKING\\` | Task is being processed | | \\`TASK\\_STATE\\_COMPLETED\\` | Task finished successfully | | \\`TASK\\_STATE\\_FAILED\\` | Task failed during processing | | \\`TASK\\_STATE\\_CANCELED\\` | Task was canceled by client | | \\`TASK\\_STATE\\_REJECTED\\` | Task was rejected by server | ### Error Responses A2A uses JSON-RPC 2.0 error format with A2A-specific error codes: | Code | Error | Description | | -------- | --------------------------------- | ---------------------------------------------------------- | | \\`-32700\\` | Parse error | Invalid JSON payload | | \\`-32600\\` | Invalid Request | Missing required fields (message.parts, message.messageId) | | \\`-32601\\` | Method not found | Unknown A2A method | | \\`-32001\\` | Task not found | Task ID does not exist | | \\`-32002\\` | Task is not in a cancelable state | CancelTask on a terminal task | Unsupported media type is returned as HTTP \\*\\*415\\*\\* (not a JSON-RPC error code). \\*\\*Example error response:\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"error\": { \"code\": -32600, \"message\": \"Invalid Request\", \"data\": \\[ { \"@type\": \"type.googleapis.com/google.rpc.BadRequest\", \"fieldViolations\": \\[ { \"field\": \"message.messageId\", \"description\": \"Message messageId is required\" } \\] } \\] } } \\`\\`\\` ### Streaming Use \\`SendStreamingMessage\\` for real-time SSE updates: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -H \"Accept: text/event-stream\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 5, \"method\": \"SendStreamingMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-5\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } }' \\`\\`\\` Response format (A2A v1.0.0 streaming sequence - exact frames the service emits): \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"task\":{\"id\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"status\":{\"state\":\"TASK\\_STATE\\_WORKING\",\"timestamp\":\"...\"},\"artifacts\":\\[\\]}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"artifactUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"artifact\":{\"artifactId\":\"artifact\\_...\",\"name\":\"conversion\\_result\",\"parts\":\\[{\"text\":\" partial \"}\\]},\"append\":true,\"lastChunk\":false}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"artifactUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"artifact\":{\"artifactId\":\"artifact\\_...\",\"name\":\"conversion\\_result\",\"parts\":\\[{\"text\":\" more \"}\\]},\"append\":true,\"lastChunk\":true}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"statusUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"status\":{\"state\":\"TASK\\_STATE\\_COMPLETED\",\"timestamp\":\"...\"}}}} data: \\[DONE\\] \\`\\`\\` The first frame carries the full \\`task\\` in TASK\\_STATE\\_WORKING; content streams as \\`artifactUpdate\\` frames (\\`lastChunk: true\\` on the final chunk); \\`statusUpdate\\` closes the stream with the terminal TASK\\_STATE\\_COMPLETED state. The persisted task (via \\`GetTask\\`) carries the real \\`artifacts\\[\\].parts\\[\\]\\` content. ### Subscribe to Task Subscribe to an existing task for real-time updates: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -H \"Accept: text/event-stream\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 6, \"method\": \"SubscribeToTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` ### Cancel Task Cancel an in-progress task: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 7, \"method\": \"CancelTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` Response: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 7, \"result\": { \"id\": \"task\\_12345\", \"contextId\": \"ctx\\_...\", \"status\": { \"state\": \"TASK\\_STATE\\_CANCELED\", \"timestamp\": \"2026-06-26T16:00:00.000Z\" } } } \\`\\`\\` ## ACP Configuration Connect mdapi.io to your IDE or coding agent (JetBrains, Cursor, VS Code, etc.) via the Agent Client Protocol v1.0.0. ACP is a JSON-RPC 2.0 endpoint at \\`POST /acp\\`. Its native surface is \\*\\*session/turn\\*\\*: create an ephemeral session, then send a prompt - the converted content streams back as \\`session/update\\` notifications. > \\*\\*Single source via \\`input\\`:\\*\\* each \\`session/prompt\\` carries a unified \\`input\\` - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). ### Basic Configuration Add to your ACP client configuration (IDE plugin / agent settings): \\`\\`\\`json { \"acpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/acp\" } } } \\`\\`\\` Or call the JSON-RPC endpoint directly with \\`POST /acp\\` (Content-Type: \\`application/json\\`). Sessions are \\*\\*stateless and ephemeral\\*\\* - nothing is persisted server-side, so each \\`session/prompt\\` runs as an independent conversion: \\`\\`\\`bash # 1. Create a session curl -X POST https://mdapi.io/acp \\\\ -H \"Content-Type: application/json\" \\\\ -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"session/new\",\"params\":{}}' # 2. Send a prompt (content streams back as session/update notifications) curl -X POST https://mdapi.io/acp \\\\ -H \"Content-Type: application/json\" \\\\ -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/prompt\",\"params\":{ \"sessionId\":\"\", \"prompt\":\\[{\"type\":\"text\",\"text\":\"\"}, {\"type\":\"resource\\_link\",\"uri\":\"https://example.com\"}\\] }}' \\`\\`\\` > \\*\\*Note:\\*\\* \\`GET /acp\\` is not supported (ACP is POST-only). A free tier is available without a token. ### ACP Methods | Method | Description | | ---------------- | ------------------------------------------------------------------------------- | | \\`initialize\\` | Handshake: return protocol version, agent capabilities, and agent info | | \\`session/new\\` | Create an ephemeral, stateless session (returns a \\`sessionId\\`) | | \\`session/prompt\\` | Run a conversion turn; content streams back as \\`session/update\\` notifications | | \\`session/cancel\\` | Notification (204, no response body) that best-effort cancels an in-flight turn | ### ACP Session Examples \\*\\*URL conversion (instructions + \\`resource\\_link\\`):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[ { \"type\": \"text\", \"text\": \"Summarize\" }, { \"type\": \"resource\\_link\", \"uri\": \"https://example.com\" } \\] } } \\`\\`\\` \\*\\*Text conversion (a bare \\`text\\` block becomes the input):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[{ \"type\": \"text\", \"text\": \"Hello World\" }\\] } } \\`\\`\\` \\*\\*File conversion (a \\`data:\\` URI in a \\`text\\` block is auto-detected as a file):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[ { \"type\": \"text\", \"text\": \"Extract the title\" }, { \"type\": \"text\", \"text\": \"data:text/plain;base64,SGVsbG8gV29ybGQ=\" } \\] } } \\`\\`\\` \\*\\*Token activation (first request only):\\*\\* every protocol passes the token per-call. Include it on any method's \\`params\\` as a \\`token\\` (+ \\`memo\\` on first use) - ACP does not use a session-level authenticate exchange. ### ACP Response Format Content is delivered \\*\\*exclusively via \\`session/update\\` notifications\\*\\*; the final \\`session/prompt\\` result carries only the \\`stopReason\\` (\\`PromptResponse\\`). A successful turn looks like this SSE stream: \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"method\":\"session/update\",\"params\":{\"sessionId\":\"\",\"update\":\\[{\"content\":{\"type\":\"text\",\"text\":\"\"},\"messageId\":\"\"}\\]}} data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"stopReason\":\"end\\_turn\"}} data: \\[DONE\\] \\`\\`\\` - The \\`session/update\\` \\`update\\[\\]\\` array holds the content chunks; \\`messageId\\` is stable across all chunks of a single turn. - \\*\\*Streaming:\\*\\* add \\`\"stream\": true\\` to the \\`session/prompt\\` \\`params\\` to stream the conversion live - each content chunk arrives as its own \\`session/update\\` notification (same \\`messageId\\`), then the \\`PromptResponse\\`. Without it, the whole turn arrives as one buffered \\`session/update\\` notification. Streaming is inherent to ACP; content only ever rides notifications. - A failed conversion streams an explanatory \\`session/update\\`, then ends with \\`stopReason: \"error\"\\`. \\`session/cancel\\` is a client→agent notification - the agent acknowledges it with HTTP 204 and no body. - Token status (\\`X-Token-Balance\\`, \\`X-Token-Expires\\`, \\`X-Token-Status\\`) is proxied from the core into the response headers. ## Usage scenarios mdapi.io is a minimal, self-documenting service-transport primitive: REST, MCP, ACP, A2A, and OpenAI-compatible endpoints all call the same transformation core, so agents can combine protocols and pass already-processed knowledge between each other. - \\*\\*Agent swarms\\*\\* - each request is handled by a stateless, automatically-scaled execution environment, so the service scales horizontally. An orchestrator fans work out across a swarm of agents, and the swarm processes very large batches of distinct resources in parallel - millions of resources in a matter of minutes, the ceiling set by how widely the work is distributed rather than by the service. Different users may freely access the same resource. - \\*\\*Shared vs individual payment\\*\\* - an orchestrator can pay once for a shared token (batching on-chain activity), or each agent can activate its own token for the exact volume it received. - \\*\\*Human-in-the-loop\\*\\* - if an agent has no wallet or insufficient funds, it returns payment details + a QR code; the human pays from a mobile device and the agent resumes. - \\*\\*Role switching\\*\\* - an agent's role can change mid-task; one agent fetches/normalizes, another summarizes/extracts, relaying compact results via the text or prompt parameters. - \\*\\*Cross-protocol interoperability\\*\\* - knowledge extracted on one protocol is reusable on another. - \\*\\*Bulk processing / model training\\*\\* - the swarm pattern turns mdapi.io into a high-throughput edge pipeline for large corpora. See https://mdapi.io/about for the full scenario walkthrough. ## Links - \\*\\*About service:\\*\\* https://mdapi.io/about - \\*\\*API docs:\\*\\* https://mdapi.io - \\*\\*MCP server manifest:\\*\\* https://mdapi.io/mcp - \\*\\*Health check:\\*\\* https://mdapi.io/health - \\*\\*API documentation:\\*\\* https://mdapi.io/llms.txt - \\*\\*Full API documentation:\\*\\* https://mdapi.io/llms-full.txt - \\*\\*AI discovery:\\*\\* https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json - \\*\\*AI Agent discovery:\\*\\* https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json - \\*\\*A2A Agent card:\\*\\* https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json - \\*\\*ACP manifest:\\*\\* https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json - \\*\\*x402 payment manifest:\\*\\* https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json - \\*\\*OpenAPI specification (JSON):\\*\\* https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json - \\*\\*OpenAPI specification (YAML):\\*\\* https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml - \\*\\*MAPI specification (case-insensitive path MAPI.md support):\\*\\* https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md - \\*\\*Skill specification (case-insensitive path SKILL.md support):\\*\\* https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md - \\*\\*Agent Plugins package (agent-plugins.org v1.0.0):\\*\\* https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/ ## External Links - \\*\\*github.com\\*\\* https://github.com/mdapiio/mdapi.io - \\*\\*skills.sh\\*\\* https://www.skills.sh/mdapiio/mdapi.io - \\*\\*skillsmp.com\\*\\* https://skillsmp.com/creators/mdapiio/mdapi.io - \\*\\*clawhub.ai\\*\\* https://clawhub.ai/mdapiio - \\*\\*x.com\\*\\* https://x.com/mdapiio ## Disclaimer \\*\\*The service is provided \"AS IS\".\\*\\* > mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.\n",
"fullPageMarkdown": "\\# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems. Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage. ## Agent entrypoint - \\*\\*Start AI discovery\\*\\* → https://mdapi.io/.well-known/ai-discovery.json - \\*\\*Use skill\\*\\* → https://mdapi.io/.well-known/skill.md ## Quick Start Choose your entry point based on your role: | Role | Protocol | Endpoint | When to use | | ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). | | AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. | | AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. | | OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. | | Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. | ### Universal discovery All protocols and capabilities are described in one file: GET /.well-known/ai-discovery.json ## Features - Stateless, in-memory processing - Edge execution with automatic scaling - Prompt-driven transformation - AI-optimized output for LLMs - Pay-per-use via x402 v1/v2 or manual payment ## Supported Formats | Type | Formats | | --------- | ------------------------------ | | Documents | PDF, DOCX, XLSX, XLS, ODT, ODS | | Images | JPEG, JPG, PNG, WebP, SVG | | Text | HTML, XML, JSON, CSV, TXT | | Webpages | Any publicly accessible URL | ## Source Parameters (all protocols) Every protocol (REST, MCP, ACP, A2A, OpenAI) converges on the \\*\\*same conversion core\\*\\*, so content is specified via a single unified \\`input\\` parameter everywhere. | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | \\`input\\` | string | Content to convert: a URL (http(s)://...), a data URI (data:mime/type;base64,...), or plain text. Auto-detected: http(s):// → URL, data: → file, otherwise → text. | The \\`input\\` parameter is auto-detected by the core: URLs (starting with \\`http://\\` or \\`https://\\`) are fetched, data URIs (starting with \\`data:\\`) are decoded as files, and anything else is treated as raw text. Additional parameters (\\`prompt\\`, \\`result\\`, \\`stream\\`, \\`token\\`, \\`memo\\`) are orthogonal and may be combined with \\`input\\`. All five protocols expose the same \\`input\\` source and apply the same transformations, streaming, and prompt-driven processing - the only difference is the transport (REST query/JSON, MCP \\`tools/call\\`, ACP \\`session/prompt\\`, OpenAI \\`messages\\`, A2A \\`message.parts\\`). ## Limits | Limit | Value | | ------------------- | -------------------------------------------------------------------------------- | | \\*\\*Max file size\\*\\* | 50 MB | | \\*\\*Max URL content\\*\\* | 50 MB | | \\*\\*Rate limit\\*\\* | 10,000 requests per hour | | \\*\\*Free tier\\*\\* | 10 requests per day (no token required), within the service’s overall free quota | | \\*\\*Paid tier\\*\\* | min $0.01 per conversion (USDC on Solana) | | \\*\\*Token validity\\*\\* | 1 year | ## Authentication \\*\\*Recommended:\\*\\* Use \\`Authorization: Bearer TOKEN\\` | Method | Use Case | | -------------------- | ---------------------------------- | | Bearer (recommended) | \\`-H \"Authorization: Bearer TOKEN\"\\` | | Header | \\`-H \"X-Token-Required: TOKEN\"\\` | ## Token Activation Before you can use a paid token, you must receive a 402 response first: | Step | Description | | ---- | ------------------------------------------------------- | | 1 | Request without token → Receive 402 with NEW token+memo | | 2 | Send USDC on Solana to wallet with memo from 402 | | 3 | Retry with EXACT token+memo from 402 → Activation | | 4 | After: use token only (no memo needed) | Important: The token+memo issued in the 402 response MUST be used exactly. Using old token or different memo will be rejected. ## API Usage ### GET / (Content conversion) Simple content conversion using query parameters. Returns Markdown directly. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \\`input\\` | string | \\* | Content to convert (URL, text, or data URI - auto-detected) | | \\`prompt\\` | string | | Custom instructions for LLM processing | | \\`result\\` | string | | Response format: \\`markdown\\`, \\`prompt\\`, or \\`both\\` | | \\`stream\\` | boolean | | Enable streaming: true for SSE response | | \\`token\\` | string | | Access token for paid tier | | \\`memo\\` | string | | Memo for token activation | \\*The \\`input\\` parameter is required.\\* > \\*\\*⚠️ Browser URL limit:\\*\\* GET requests with long \\`input\\` or \\`prompt\\` values may exceed browser URL limits (~2048 characters). Use POST with JSON body for large payloads. ### POST / (Content conversion via JSON) Supports content conversion via JSON body. The \\`input\\` parameter accepts URLs, text, or data URIs (auto-detected). Returns a JSON object containing the Markdown content. #### Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | \\`input\\` | string | \\* | Content to convert (URL, text, or data URI - auto-detected) | | \\`prompt\\` | string | | Custom instructions for LLM processing | | \\`result\\` | string | | Response format: \\`markdown\\`, \\`prompt\\`, or \\`both\\` | | \\`stream\\` | boolean | | Enable streaming: true for SSE response | | \\`token\\` | string | | Access token for paid tier | | \\`memo\\` | string | | Memo for token activation | \\*The \\`input\\` parameter is required.\\* ### Result Format Parameter The \\`result\\` parameter controls the response format for both GET and POST requests. | Value | Description | | -------------------- | --------------------------------------------------------------- | | \\`markdown\\` (default) | Returns the converted Markdown content | | \\`prompt\\` | Returns the result of LLM processing with \\`prompt\\` instructions | | \\`both\\` | Returns both \\`markdown\\` and \\`prompt\\_result\\` in the response | When \\`result=both\\`: - \\*\\*GET requests\\*\\* return Markdown combining \\`markdown\\`, followed by \"## Prompt Result\" and \\`prompt\\_result\\` (always in Markdown format) - \\*\\*POST requests\\*\\* return JSON with \\`markdown\\` and \\`prompt\\_result\\` fields > \\*\\*Auto \\`result\\`:\\*\\* When \\`prompt\\` is provided without an explicit \\`result\\`, the core automatically sets \\`result=\"prompt\"\\` (LLM output only). Without \\`prompt\\`, default is \\`result=\"markdown\"\\`. Only specify \\`result\\` explicitly when you need both (\\`result=\"both\"\\`). ### Prompt Parameter The \\`prompt\\` parameter lets you specify custom instructions for the LLM to follow when generating the result. | Use Case | Example | | ------------------ | ---------------------------------------------------------- | | Summarize | \\`?input=https://example.com&prompt=Summarize\\` | | Extract key points | \\`?input=Hello World&prompt=Extract key points\\` | | Convert to JSON | \\`?input=https://example.com&prompt=Convert to JSON format\\` | | Analyze content | \\`?input=Hello World&prompt=Analyze and explain\\` | ### Streaming Parameter The \\`stream\\` parameter enables Server-Sent Events (SSE) streaming for real-time response delivery. \\*\\*Type:\\*\\* \\`boolean\\` \\*\\*Default:\\*\\* \\`false\\` (non-streaming) Example: \\`\\`\\`bash curl \"https://mdapi.io/?input=...&stream=true\" \\`\\`\\` Response format (OpenAI-compatible SSE, one JSON object per \\`data:\\` line): \\`\\`\\`json data: {\"type\":\"token\\_info\",\"token\\_status\":\"valid\",\"token\\_balance\":0.99,\"token\\_expires\":1798761600} data: {\"choices\":\\[{\"index\":0,\"delta\":{\"content\":\" chunk\"},\"finish\\_reason\":null}\\]} data: {\"choices\":\\[{\"index\":0,\"delta\":{\"content\":\" more\"},\"finish\\_reason\":null}\\]} data: {\"choices\":\\[{\"index\":0,\"delta\":{},\"finish\\_reason\":\"stop\"}\\]} data: \\[DONE\\] \\`\\`\\` \\*\\*Native streaming per protocol.\\*\\* Every protocol delivers a \\*real\\* content stream when \\`stream: true\\`, but each emits it in its own native frame format (so existing clients keep working): | Protocol | Streaming frame format | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | REST | OpenAI-compatible \\`choices/delta\\` frames | | OpenAI | \\`chat.completion.chunk\\` (\\`choices/delta\\`) | | MCP | \\`notifications/message\\` content chunks, then one final \\`tools/call\\` result frame | | ACP | \\`session/update\\` notification chunks (one stable \\`messageId\\` per turn), then a final response carrying only \\`stopReason\\` | | A2A | \\`result.task\\` (\\`TASK\\_STATE\\_WORKING\\`) start frame, \\`result.artifactUpdate\\` (\\`{artifact, append, lastChunk}\\`) content frames, then \\`result.statusUpdate\\` (\\`TASK\\_STATE\\_COMPLETED\\`) - stream closes | > \\*\\*Note on MCP transport vs. the \\`stream\\` parameter.\\*\\* The MCP manifest advertises \\`transport.type: \"streamable-http\"\\` - that is the MCP \\*transport\\* > (how JSON-RPC requests are delivered to \\`POST /mcp\\`). It is unrelated to the \\`stream\\` \\*parameter\\*, which independently enables SSE streaming of the > conversion \\*\\*content\\*\\*. You can use MCP without streaming; and when you do pass \\`stream: true\\`, the content arrives as SSE frames alongside the transport. ### Response Codes | Code | Description | Response Body (GET) | Response Body (POST) | | ---- | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | | 200 | Success | Markdown content | JSON with \\`success\\`, markdown, prompt\\_result, metrics, token fields | | 402 | Payment Required | Markdown payment instructions (\\`X-Error-Code: payment\\_required\\`) | JSON with \\`success:false\\`, \\`code:\"payment\\_required\"\\`, and payment object | | 400 | Bad Request | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"invalid\\_request\"}\\` | | 401 | Invalid Token | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"unauthorized\"}\\` | | 404 | Not Found | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"not\\_found\"}\\` | | 413 | Payload Too Large | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"too\\_large\"}\\` | | 429 | Rate Limited | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"rate\\_limited\"}\\` | | 500 | Server Error | Markdown error + \\`X-Error-Code\\` header | JSON \\`{\"success\":false,\"error\":\"...\",\"code\":\"server\\_error\"}\\` | ### Token Status The X-Token-Status header (and token\\_status field in responses) indicates the current state of authentication: | Status | Description | | ------------------ | --------------------------------------------------------------------------------------- | | free | Free tier (no token required, 10 requests/day), within the service's overall free quota | | valid | Paid token active with remaining balance | | invalid | Token not found or not provided | | expired | Token validity period has ended | | exhausted | Token balance has been fully used | | expired\\_pending | Activation memo has expired | | activated | Token was just activated with this request | | verification\\_error | Payment verification failed | | invalid\\_payment | Payment transaction is invalid | | error | Internal error during token processing | | pending | Payment required (token not yet activated) | ### Endpoints | Method | Path | Description | | ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | GET | /about | About service | | GET | / | API docs or conversion (via query parameter: \\`input\\`, \\`prompt\\`, \\`result\\`) | | POST | / | Convert content via JSON body (supports \\`input\\`, \\`prompt\\`, \\`result\\` params) | | POST | /v1/chat/completions | OpenAI-compatible endpoint | | GET | /mcp | MCP server manifest | | POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) | | POST | /acp | ACP RPC endpoint (IDE agents) | | POST | /a2a | A2A RPC endpoint (agent2agent) | | GET | /health | Health check | | GET | /llms.txt | API documentation | | GET | /llms-full.txt | Full API documentation | | GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery | | GET | /.well-known/agent.json or /agent.json | AI Agent discovery | | GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card | | GET | /.well-known/acp.json or /acp.json | ACP manifest | | GET | /.well-known/x402.json or /x402.json | x402 payment manifest | | GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) | | GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) | | GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) | | GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) | | GET | /.well-known/skills/index.json | Legacy skills index | | GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) | | GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) | | GET | /.well-known/mcp/server-cards.json | MCP server cards index | | GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest | | GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config | | GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill | #### Examples \\`\\`\\`bash # URL conversion via GET (free) curl \"https://mdapi.io/?input=https://example.com\" # URL with prompt and result=both (returns markdown + prompt\\_result) curl \"https://mdapi.io/?input=https://example.com&prompt=Summarize&result=both\" # Text with prompt (auto result=prompt) curl \"https://mdapi.io/?input=Hello World&prompt=Summarize\" # Token activation via GET (activate and use) curl -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" \"https://mdapi.io/?input=https://example.com\" # Paid request with token via GET (using token) curl -H \"Authorization: Bearer YOUR\\_TOKEN\" \"https://mdapi.io/?input=https://example.com\" # URL conversion via POST (free) curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" # Text with prompt via POST curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"Hello World\",\"prompt\":\"Summarize\",\"result\":\"both\"}' \"https://mdapi.io/\" # File upload via POST (data URI) curl -X POST -H \"Content-Type: application/json\" -d '{\"input\":\"data:text/plain;base64,SGVsbG8gV29ybGQ=\"}' \"https://mdapi.io/\" # Token activation via POST curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" # Paid request with token via POST curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -d '{\"input\":\"https://example.com\"}' \"https://mdapi.io/\" \\`\\`\\` ### OpenAI Compatible Endpoint The \\`/v1/chat/completions\\` endpoint provides an OpenAI‑compatible API for markdown conversion with streaming support. \\*\\*Supported features:\\*\\* - URL extraction from message content (any text containing https?://) - image\\_url in messages (OpenAI format) - supports HTTP URLs and data URLs - file in messages (OpenAI format) - base64 encoded files (field \\`file.data\\`, optional \\`file.mimeType\\`; built into a \\`data:\\` URI for the core - no \\`file.filename\\` required or used) - Direct text content in messages (any text without a URL is sent to the core as the \\`input\\` source and converted to Markdown) - Token and memo via headers (recommended for POST) - Streaming SSE responses (\\`stream: true\\`) - Custom instructions with LLM processing (system messages, or user messages containing instruction keywords such as \\*extract, summarize, analyze, format, convert to, write as, create, generate, json\\* → LLM-driven summary/extraction/transformation) - \\`prompt\\` for LLM-processed output. The response surfaces \\`prompt\\_result\\` at the top level alongside the standard \\`choices\\[\\].message.content\\` (which carries \\`prompt\\_result\\` when prompt is set, otherwise the Markdown). \\`model\\` is accepted but not required (any string; the service uses its own conversion pipeline, not a remote LLM chat model, unless custom instructions trigger LLM processing). > \\*\\*Content via message text:\\*\\* the message text is passed to the core as the \\`input\\` parameter - the same unified source as every other protocol. URLs are auto-detected, data URIs are decoded as files, and plain text is processed directly. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). #### Request Schema \\`\\`\\`json { \"type\": \"object\", \"properties\": { \"model\": { \"type\": \"string\", \"description\": \"Optional model identifier (any string accepted; not required)\" }, \"messages\": { \"type\": \"array\", \"items\": { \"type\": \"object\", \"properties\": { \"role\": {\"type\": \"string\", \"enum\": \\[\"user\", \"system\", \"assistant\"\\]}, \"content\": {\"oneOf\": \\[{\"type\": \"string\"}, {\"type\": \"array\"}\\]} } }, \"description\": \"Chat messages. URL in content, image\\_url or file in content for input\" }, \"stream\": { \"type\": \"boolean\", \"default\": false, \"description\": \"Enable streaming SSE responses\" }, \"prompt\": { \"type\": \"string\", \"description\": \"Custom instructions for LLM processing (alternative to instruction keywords in messages)\" }, \"result\": { \"type\": \"string\", \"enum\": \\[\"markdown\", \"prompt\", \"both\"\\], \"description\": \"Response format when using prompt\" }, \"token\": {\"type\": \"string\", \"description\": \"Access token for paid tier\"}, \"memo\": {\"type\": \"string\", \"description\": \"Memo for token activation\"}, \"input\": { \"type\": \"string\", \"description\": \"Content to convert (URL, text, or data URI - auto-detected). Alternative to a URL/file embedded in messages\" } }, \"required\": \\[\"messages\"\\] } \\`\\`\\` > \\*\\*Note on streaming + custom instructions:\\*\\* when custom instructions trigger LLM processing, the response is returned as a single completion (streaming is not applied to the LLM pass). Streaming SSE applies to the standard conversion path. ## MCP Configuration Connect mdapi.io to your MCP-compatible client (spec 2026-07-28, stateless). > \\*\\*Single source via \\`input\\`:\\*\\* the \\`convert\\` tool accepts a unified \\`input\\` parameter - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). ### Protocol Requirements - \\*\\*Transport:\\*\\* Streamable HTTP (POST-only for JSON-RPC, GET for manifest) - \\*\\*Required headers:\\*\\* \\`MCP-Protocol-Version: 2026-07-28\\` and \\`Mcp-Method\\` on every request; \\`Mcp-Name\\` additionally on \\`tools/call\\`, \\`resources/read\\`, and \\`prompts/get\\` - \\*\\*Stateless:\\*\\* No sessions - every request is independent - \\*\\*Discovery:\\*\\* Use \\`server/discover\\` to query server capabilities and supported versions ### Basic Configuration Add to your MCP config file: \\`\\`\\`json { \"mcpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/mcp\" } } } \\`\\`\\` > \\*\\*Note:\\*\\* No token is required to connect. A free tier is available (10 requests per day), within the service’s overall free quota. ### OpenClaw Integration OpenClaw can use mdapi.io in two ways: \\*\\*Option 1: Via MCP (Recommended)\\*\\* \\`\\`\\`json { \"mcpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/mcp\" } } } \\`\\`\\` \\*\\*Option 2: Via OpenAI-compatible endpoint\\*\\* \\`\\`\\`bash openclaw config set llm.apiBase https://mdapi.io/v1 openclaw config set llm.apiKey YOUR\\_TOKEN \\`\\`\\` ### Using MCP with a token MCP does not use HTTP-level Authorization headers. The token is always passed inside the tool \\`arguments\\` object. \\*\\*Activation\\*\\* - include \\`token\\` + \\`memo\\` in the first request: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_TOKEN\", \"memo\": \"YOUR\\_PAYMENT\\_MEMO\" } } } \\`\\`\\` \\*\\*After activation\\*\\* - use \\`token\\` only (no memo needed): \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_ACTIVATED\\_TOKEN\" } } } \\`\\`\\` ### MCP Tool Examples Convert with prompt and result: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"prompt\": \"Summarize\", \"result\": \"both\", \"token\": \"YOUR\\_TOKEN\" } } } \\`\\`\\` Process text directly: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"Hello World\", \"prompt\": \"Extract key points\", \"result\": \"prompt\" } } } \\`\\`\\` Stream with SSE (native MCP frames): \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"tools/call\", \"params\": { \"name\": \"convert\", \"arguments\": { \"input\": \"https://example.com\", \"stream\": true } } } \\`\\`\\` Response (SSE over \\`streamable-http\\`): intermediate \\`notifications/message\\` content chunks, then one final \\`tools/call\\` result frame with the full Markdown: \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\",\"data\":\" partial \"}} data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\",\"data\":\" more \"}} data: {\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"content\":\\[{\"type\":\"text\",\"text\":\"\"}\\],\"isError\":false}} data: \\[DONE\\] \\`\\`\\` ### Using Environment Variables Or use environment variable: \\`\\`\\`bash export MDAPI\\_TOKEN=YOUR\\_ACTIVATED\\_TOKEN \\`\\`\\` ## Code Examples ### JavaScript (fetch) \\`\\`\\`javascript // Convert a URL via GET - returns Markdown directly const response = await fetch('https://mdapi.io/?input=https://example.com'); const markdown = await response.text(); console.log(markdown); \\`\\`\\` \\`\\`\\`javascript // Convert a URL via POST - returns JSON with metadata const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); console.log(data.markdown); \\`\\`\\` \\`\\`\\`javascript // Text with prompt - returns prompt\\_result const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'Hello World', prompt: 'Summarize', result: 'both' }) }); const data = await response.json(); console.log(data.markdown); console.log(data.prompt\\_result); \\`\\`\\` \\`\\`\\`javascript // File upload via data URI const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: 'data:text/plain;base64,SGVsbG8gV29ybGQ=' }) }); const data = await response.json(); console.log(data.markdown); \\`\\`\\` \\`\\`\\`javascript // Token activation - first request with token + memo const response = await fetch('https://mdapi.io/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\\_TOKEN', 'X-Memo-Required': 'YOUR\\_MEMO' }, body: JSON.stringify({ input: 'https://example.com' }) }); const data = await response.json(); // After activation, use token only (no memo needed) \\`\\`\\` \\`\\`\\`javascript // Streaming via OpenAI-compatible endpoint const response = await fetch('https://mdapi.io/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR\\_TOKEN' }, body: JSON.stringify({ model: 'markdown-v1', messages: \\[{ role: 'user', content: 'Convert https://example.com' }\\], stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; process.stdout.write(decoder.decode(value)); } \\`\\`\\` ### Python \\`\\`\\`python import requests # Convert a URL via GET - returns Markdown directly response = requests.get('https://mdapi.io/?input=https://example.com') response.raise\\_for\\_status() print(response.text) \\`\\`\\` \\`\\`\\`python import requests # Convert a URL via POST - returns JSON with metadata response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) \\`\\`\\` \\`\\`\\`python import requests # Text with prompt - returns prompt\\_result response = requests.post( 'https://mdapi.io/', json={'input': 'Hello World', 'prompt': 'Summarize', 'result': 'both'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) print(data\\['prompt\\_result'\\]) \\`\\`\\` \\`\\`\\`python import requests # File upload via data URI with open('document.pdf', 'rb') as f: import base64 file\\_data = base64.b64encode(f.read()).decode() response = requests.post( 'https://mdapi.io/', json={'input': f'data:text/plain;base64,{file\\_data}'} ) response.raise\\_for\\_status() data = response.json() print(data\\['markdown'\\]) \\`\\`\\` \\`\\`\\`python import requests # Token activation response = requests.post( 'https://mdapi.io/', json={'input': 'https://example.com'}, headers={ 'Authorization': 'Bearer YOUR\\_TOKEN', 'X-Memo-Required': 'YOUR\\_MEMO' } ) response.raise\\_for\\_status() # After activation, use token only (no memo needed) \\`\\`\\` \\`\\`\\`python from openai import OpenAI # OpenAI-compatible streaming client = OpenAI(base\\_url='https://mdapi.io/v1', api\\_key='YOUR\\_TOKEN') stream = client.chat.completions.create( model='markdown-v1', messages=\\[{'role': 'user', 'content': 'Convert https://example.com'}\\], stream=True ) for chunk in stream: if chunk.choices\\[0\\].delta.content: print(chunk.choices\\[0\\].delta.content, end='') \\`\\`\\` ### Go \\`\\`\\`go package main import ( \"fmt\" \"io\" \"net/http\" ) func main() { // Convert a URL via GET - returns Markdown directly resp, err := http.Get(\"https://mdapi.io/?input=https://example.com\") if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() body, \\_ := io.ReadAll(resp.Body) fmt.Println(string(body)) } \\`\\`\\` \\`\\`\\`go package main import ( \"bytes\" \"encoding/json\" \"fmt\" \"net/http\" ) func main() { // Convert a URL via POST - returns JSON with metadata payload, \\_ := json.Marshal(map\\[string\\]string{ \"input\": \"https://example.com\", }) resp, err := http.Post( \"https://mdapi.io/\", \"application/json\", bytes.NewReader(payload), ) if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() var result map\\[string\\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\\[\"markdown\"\\]) } \\`\\`\\` \\`\\`\\`go package main import ( \"bytes\" \"encoding/base64\" \"encoding/json\" \"fmt\" \"net/http\" \"os\" ) func main() { // File upload via data URI fileBytes, \\_ := os.ReadFile(\"document.pdf\") b64 := base64.StdEncoding.EncodeToString(fileBytes) payload, \\_ := json.Marshal(map\\[string\\]string{ \"input\": \"data:text/plain;base64,\" + b64, }) resp, err := http.Post( \"https://mdapi.io/\", \"application/json\", bytes.NewReader(payload), ) if err != nil { fmt.Println(\"HTTP error:\", err) return } defer resp.Body.Close() var result map\\[string\\]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result\\[\"markdown\"\\]) } \\`\\`\\` ### Rust \\`\\`\\`rust use anyhow::Result; use reqwest::Client; #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let url = \"https://mdapi.io/?input=https://example.com\"; let response = client.get(url).send().await?; response.error\\_for\\_status\\_ref()?; let markdown = response.text().await?; println!(\"{}\", markdown); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::{Result, Context}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #\\[derive(Deserialize)\\] struct ApiResponse { markdown: Option, prompt\\_result: Option, } #\\[derive(Serialize)\\] struct ConvertRequest { input: String, #\\[serde(skip\\_serializing\\_if = \"Option::is\\_none\")\\] prompt: Option, #\\[serde(skip\\_serializing\\_if = \"Option::is\\_none\")\\] result: Option, } pub struct MdApiClient { client: Client, base\\_url: String, token: Option, } impl MdApiClient { pub fn new(token: Option) -> Self { Self { client: Client::new(), base\\_url: \"https://mdapi.io\".to\\_string(), token, } } pub async fn convert\\_url( &self, url: &str, prompt: Option<&str>, ) -> Result { let body = ConvertRequest { input: url.to\\_string(), prompt: prompt.map(|p| p.to\\_string()), result: prompt.map(|\\_| \"both\".to\\_string()), }; let mut request = self.client .post(&self.base\\_url) .header(\"Content-Type\", \"application/json\") .json(&body); if let Some(ref token) = self.token { request = request.header(\"Authorization\", format!(\"Bearer {}\", token)); } let response = request .send() .await .context(\"Failed to send HTTP request\")?; response .error\\_for\\_status\\_ref() .context(\"API returned error status\")?; response .text() .await .context(\"Failed to read response body\") } } // Example: convert a URL with prompt #\\[tokio::main\\] async fn main() -> Result<()> { let token = std::env::var(\"MDAPI\\_TOKEN\").ok(); let client = MdApiClient::new(token); let markdown = client .convert\\_url(\"https://example.com\", Some(\"Summarize\")) .await?; println!(\"{}\", markdown); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::Result; use reqwest::Client; use base64::engine::general\\_purpose::STANDARD; use base64::Engine; #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let file\\_bytes = std::fs::read(\"document.pdf\")?; let b64 = STANDARD.encode(&file\\_bytes); let body = serde\\_json::json!({ \"input\": format!(\"data:text/plain;base64,{}\", b64) }); let response = client .post(\"https://mdapi.io/\") .header(\"Content-Type\", \"application/json\") .json(&body) .send() .await?; let data: serde\\_json::Value = response.json().await?; println!(\"{}\", data\\[\"markdown\"\\]); Ok(()) } \\`\\`\\` \\`\\`\\`rust use anyhow::Result; use futures\\_util::stream::StreamExt; use reqwest::Client; // Streaming response when using the OpenAI‑compatible endpoint with stream = true #\\[tokio::main\\] async fn main() -> Result<()> { let client = Client::new(); let token = std::env::var(\"MDAPI\\_TOKEN\")?; let body = serde\\_json::json!({ \"model\": \"markdown-v1\", \"messages\": \\[ { \"role\": \"user\", \"content\": \"Convert https://example.com\" } \\], \"stream\": true }); let response = client .post(\"https://mdapi.io/v1/chat/completions\") .header(\"Authorization\", format!(\"Bearer {}\", token)) .json(&body) .send() .await?; response.error\\_for\\_status\\_ref()?; let mut stream = response.bytes\\_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; let text = String::from\\_utf8\\_lossy(&chunk); eprint!(\"{}\", text); } Ok(()) } \\`\\`\\` ### OpenAI SDK \\`\\`\\`python from openai import OpenAI client = OpenAI( base\\_url=\"https://mdapi.io/v1\", api\\_key=\"YOUR\\_TOKEN\" ) try: response = client.chat.completions.create( model=\"markdown-v1\", messages=\\[{\"role\": \"user\", \"content\": \"Convert https://example.com\"}\\] ) print(response.choices\\[0\\].message.content) except Exception as e: print(\"API error:\", e) \\`\\`\\` #### OpenAI with paid token \\`\\`\\`bash curl -X POST \"https://mdapi.io/v1/chat/completions\" -H \"Authorization: Bearer YOUR\\_TOKEN\" -H \"X-Memo-Required: YOUR\\_MEMO\" -H \"Content-Type: application/json\" -d '{\"model\":\"markdown-v1\",\"messages\":\\[{\"role\":\"user\",\"content\":\"Convert https://example.com\"}\\]}' \\`\\`\\` After activation, use token only (no memo needed): \\`\\`\\`bash curl -X POST \"https://mdapi.io/v1/chat/completions\" -H \"Authorization: Bearer YOUR\\_ACTIVATED\\_TOKEN\" -H \"Content-Type: application/json\" -d '{\"model\":\"markdown-v1\",\"messages\":\\[{\"role\":\"user\",\"content\":\"Convert https://example.com\"}\\]}' \\`\\`\\` ## A2A Configuration Connect mdapi.io to your A2A-compatible agent (Claude Code, Codex, OpenClaw, Hermes, etc.). ### Basic Configuration Add to your A2A client configuration: \\`\\`\\`json { \"agent\": { \"name\": \"mdapi\", \"agentCard\": { \"url\": \"https://mdapi.io/.well-known/agent-card.json\" } } } \\`\\`\\` Or use JSON-RPC directly: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-1\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } } \\`\\`\\` ### A2A Methods | Method | Description | | -------------------- | --------------------------------------- | | SendMessage | Send a message to initiate conversion | | SendStreamingMessage | Send message with SSE streaming updates | | GetTask | Get task status and results by ID | | ListTasks | List tasks with optional filtering | | CancelTask | Cancel an in-progress task | | SubscribeToTask | Subscribe to task updates via SSE | > \\*\\*Single source via \\`input\\`:\\*\\* the \\`input\\` parameter in the message parts is the unified source - the same as the REST endpoint. A bare URL > inside a text part (e.g. \\`\"Convert https://example.com\"\\`) is extracted automatically and used as the conversion source, so you don't need to wrap it > in structured JSON. Instructions such as \\`Summarize\\` should be passed via the structured \\`{ \"input\": \"...\", \"prompt\": \"...\" }\\` form, not mixed into the text. ### A2A Examples #### SendMessage \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-1\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } }' \\`\\`\\` #### SendMessage with file (data URI) \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-2\", \"parts\": \\[ { \"text\": \"{\"input\":\"data:text/plain;base64,SGVsbG8gV29ybGQ=\"}\" } \\] } } }' \\`\\`\\` #### SendMessage with structured data \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-3\", \"parts\": \\[ { \"data\": { \"input\": \"https://example.com\", \"result\": \"markdown\" }, \"mediaType\": \"application/json\" } \\] } } }' \\`\\`\\` #### Token activation via A2A \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 7, \"method\": \"SendMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-7\", \"parts\": \\[ { \"data\": { \"input\": \"https://example.com\", \"token\": \"YOUR\\_TOKEN\", \"memo\": \"YOUR\\_PAYMENT\\_MEMO\" }, \"mediaType\": \"application/json\" } \\] } } }' \\`\\`\\` > \\*\\*Note on token activation:\\*\\* Pass \\`token\\` and \\`memo\\` inside a \\`data\\` Part or as JSON inside a \\`text\\` Part. A2A does not use HTTP-level Authorization headers. #### Multi-turn conversation (follow-up) \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 4, \"method\": \"SendMessage\", \"params\": { \"contextId\": \"ctx-uuid-1\", \"message\": { \"messageId\": \"msg-uuid-4\", \"parts\": \\[ { \"text\": \"Now convert the tables to JSON\" } \\] } } }' \\`\\`\\` #### GetTask \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 5, \"method\": \"GetTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` #### ListTasks \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 6, \"method\": \"ListTasks\", \"params\": { \"contextId\": \"ctx\\_12345\", \"pageSize\": 10 } }' \\`\\`\\` ### A2A Message Parts Messages use the A2A \\`Part\\` format (field-name discriminators per spec v1.0.0): | Type | Description | Fields | | ------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------- | | \\`text\\` | Plain text content or JSON-encoded params | \\`text\\` | | \\`raw\\` | File content as base64 bytes; normalized to a data URI (\\`data:;base64,\\`) for the core | \\`raw\\` (base64), \\`mediaType\\` (optional) | | \\`data\\` | Structured JSON data (core params) | \\`data\\` (object), \\`mediaType\\` (optional) | | \\`url\\` | URL to fetch and convert | \\`url\\` (http/https) | \\*\\*Part → Core Parameter Mapping:\\*\\* - \\`text\\` Part → \\`input\\` param (direct content) or JSON-encoded params (\\`{ \"input\": \"...\", \"prompt\": \"...\" }\\`) - \\`data\\` Part → merged as params (\\`input\\`, \\`prompt\\`, \\`result\\`, \\`token\\`, \\`memo\\`, etc.) - \\`url\\` Part → \\`input\\` param (fetched and converted) ### Message Object \\`\\`\\`typescript interface Message { messageId: string; // REQUIRED: unique ID (e.g. \"msg-uuid\") contextId?: string; // Optional: group related tasks taskId?: string; // Optional: associate with existing task role: \"user\" | \"agent\"; // REQUIRED parts: Array; // REQUIRED: at least one part } \\`\\`\\` ### Task Data Model \\`\\`\\`typescript interface Task { id: string; // \"task\\_\\_\" contextId: string; // \"ctx\\_\\_\" status: { state: string; // \"TASK\\_STATE\\_WORKING\" | \"TASK\\_STATE\\_COMPLETED\" | \"TASK\\_STATE\\_FAILED\" | \"TASK\\_STATE\\_CANCELED\" | \"TASK\\_STATE\\_REJECTED\" timestamp: string; // ISO 8601 message?: Message; // only on failure }; artifacts?: Array<{ artifactId: string; name: string; parts: Array; }>; history?: Array; } \\`\\`\\` ### Task States | State | Description | | ---------------------- | ----------------------------- | | \\`TASK\\_STATE\\_WORKING\\` | Task is being processed | | \\`TASK\\_STATE\\_COMPLETED\\` | Task finished successfully | | \\`TASK\\_STATE\\_FAILED\\` | Task failed during processing | | \\`TASK\\_STATE\\_CANCELED\\` | Task was canceled by client | | \\`TASK\\_STATE\\_REJECTED\\` | Task was rejected by server | ### Error Responses A2A uses JSON-RPC 2.0 error format with A2A-specific error codes: | Code | Error | Description | | -------- | --------------------------------- | ---------------------------------------------------------- | | \\`-32700\\` | Parse error | Invalid JSON payload | | \\`-32600\\` | Invalid Request | Missing required fields (message.parts, message.messageId) | | \\`-32601\\` | Method not found | Unknown A2A method | | \\`-32001\\` | Task not found | Task ID does not exist | | \\`-32002\\` | Task is not in a cancelable state | CancelTask on a terminal task | Unsupported media type is returned as HTTP \\*\\*415\\*\\* (not a JSON-RPC error code). \\*\\*Example error response:\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"error\": { \"code\": -32600, \"message\": \"Invalid Request\", \"data\": \\[ { \"@type\": \"type.googleapis.com/google.rpc.BadRequest\", \"fieldViolations\": \\[ { \"field\": \"message.messageId\", \"description\": \"Message messageId is required\" } \\] } \\] } } \\`\\`\\` ### Streaming Use \\`SendStreamingMessage\\` for real-time SSE updates: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -H \"Accept: text/event-stream\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 5, \"method\": \"SendStreamingMessage\", \"params\": { \"message\": { \"messageId\": \"msg-uuid-5\", \"parts\": \\[ { \"text\": \"Convert https://example.com\" } \\] } } }' \\`\\`\\` Response format (A2A v1.0.0 streaming sequence - exact frames the service emits): \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"task\":{\"id\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"status\":{\"state\":\"TASK\\_STATE\\_WORKING\",\"timestamp\":\"...\"},\"artifacts\":\\[\\]}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"artifactUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"artifact\":{\"artifactId\":\"artifact\\_...\",\"name\":\"conversion\\_result\",\"parts\":\\[{\"text\":\" partial \"}\\]},\"append\":true,\"lastChunk\":false}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"artifactUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"artifact\":{\"artifactId\":\"artifact\\_...\",\"name\":\"conversion\\_result\",\"parts\":\\[{\"text\":\" more \"}\\]},\"append\":true,\"lastChunk\":true}}} data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"statusUpdate\":{\"taskId\":\"task\\_...\",\"contextId\":\"ctx\\_...\",\"status\":{\"state\":\"TASK\\_STATE\\_COMPLETED\",\"timestamp\":\"...\"}}}} data: \\[DONE\\] \\`\\`\\` The first frame carries the full \\`task\\` in TASK\\_STATE\\_WORKING; content streams as \\`artifactUpdate\\` frames (\\`lastChunk: true\\` on the final chunk); \\`statusUpdate\\` closes the stream with the terminal TASK\\_STATE\\_COMPLETED state. The persisted task (via \\`GetTask\\`) carries the real \\`artifacts\\[\\].parts\\[\\]\\` content. ### Subscribe to Task Subscribe to an existing task for real-time updates: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -H \"Accept: text/event-stream\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 6, \"method\": \"SubscribeToTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` ### Cancel Task Cancel an in-progress task: \\`\\`\\`bash curl -X POST https://mdapi.io/a2a -H \"Content-Type: application/a2a+json\" -d '{ \"jsonrpc\": \"2.0\", \"id\": 7, \"method\": \"CancelTask\", \"params\": { \"id\": \"task\\_12345\" } }' \\`\\`\\` Response: \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 7, \"result\": { \"id\": \"task\\_12345\", \"contextId\": \"ctx\\_...\", \"status\": { \"state\": \"TASK\\_STATE\\_CANCELED\", \"timestamp\": \"2026-06-26T16:00:00.000Z\" } } } \\`\\`\\` ## ACP Configuration Connect mdapi.io to your IDE or coding agent (JetBrains, Cursor, VS Code, etc.) via the Agent Client Protocol v1.0.0. ACP is a JSON-RPC 2.0 endpoint at \\`POST /acp\\`. Its native surface is \\*\\*session/turn\\*\\*: create an ephemeral session, then send a prompt - the converted content streams back as \\`session/update\\` notifications. > \\*\\*Single source via \\`input\\`:\\*\\* each \\`session/prompt\\` carries a unified \\`input\\` - the same source as every other protocol. The core auto-detects whether the value is a URL, data URI, or text. > See \\[Source Parameters (all protocols)\\](#source-parameters-all-protocols). ### Basic Configuration Add to your ACP client configuration (IDE plugin / agent settings): \\`\\`\\`json { \"acpServers\": { \"mdapi\": { \"url\": \"https://mdapi.io/acp\" } } } \\`\\`\\` Or call the JSON-RPC endpoint directly with \\`POST /acp\\` (Content-Type: \\`application/json\\`). Sessions are \\*\\*stateless and ephemeral\\*\\* - nothing is persisted server-side, so each \\`session/prompt\\` runs as an independent conversion: \\`\\`\\`bash # 1. Create a session curl -X POST https://mdapi.io/acp \\\\ -H \"Content-Type: application/json\" \\\\ -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"session/new\",\"params\":{}}' # 2. Send a prompt (content streams back as session/update notifications) curl -X POST https://mdapi.io/acp \\\\ -H \"Content-Type: application/json\" \\\\ -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/prompt\",\"params\":{ \"sessionId\":\"\", \"prompt\":\\[{\"type\":\"text\",\"text\":\"\"}, {\"type\":\"resource\\_link\",\"uri\":\"https://example.com\"}\\] }}' \\`\\`\\` > \\*\\*Note:\\*\\* \\`GET /acp\\` is not supported (ACP is POST-only). A free tier is available without a token. ### ACP Methods | Method | Description | | ---------------- | ------------------------------------------------------------------------------- | | \\`initialize\\` | Handshake: return protocol version, agent capabilities, and agent info | | \\`session/new\\` | Create an ephemeral, stateless session (returns a \\`sessionId\\`) | | \\`session/prompt\\` | Run a conversion turn; content streams back as \\`session/update\\` notifications | | \\`session/cancel\\` | Notification (204, no response body) that best-effort cancels an in-flight turn | ### ACP Session Examples \\*\\*URL conversion (instructions + \\`resource\\_link\\`):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[ { \"type\": \"text\", \"text\": \"Summarize\" }, { \"type\": \"resource\\_link\", \"uri\": \"https://example.com\" } \\] } } \\`\\`\\` \\*\\*Text conversion (a bare \\`text\\` block becomes the input):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[{ \"type\": \"text\", \"text\": \"Hello World\" }\\] } } \\`\\`\\` \\*\\*File conversion (a \\`data:\\` URI in a \\`text\\` block is auto-detected as a file):\\*\\* \\`\\`\\`json { \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"session/prompt\", \"params\": { \"sessionId\": \"\", \"prompt\": \\[ { \"type\": \"text\", \"text\": \"Extract the title\" }, { \"type\": \"text\", \"text\": \"data:text/plain;base64,SGVsbG8gV29ybGQ=\" } \\] } } \\`\\`\\` \\*\\*Token activation (first request only):\\*\\* every protocol passes the token per-call. Include it on any method's \\`params\\` as a \\`token\\` (+ \\`memo\\` on first use) - ACP does not use a session-level authenticate exchange. ### ACP Response Format Content is delivered \\*\\*exclusively via \\`session/update\\` notifications\\*\\*; the final \\`session/prompt\\` result carries only the \\`stopReason\\` (\\`PromptResponse\\`). A successful turn looks like this SSE stream: \\`\\`\\` data: {\"jsonrpc\":\"2.0\",\"method\":\"session/update\",\"params\":{\"sessionId\":\"\",\"update\":\\[{\"content\":{\"type\":\"text\",\"text\":\"\"},\"messageId\":\"\"}\\]}} data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"stopReason\":\"end\\_turn\"}} data: \\[DONE\\] \\`\\`\\` - The \\`session/update\\` \\`update\\[\\]\\` array holds the content chunks; \\`messageId\\` is stable across all chunks of a single turn. - \\*\\*Streaming:\\*\\* add \\`\"stream\": true\\` to the \\`session/prompt\\` \\`params\\` to stream the conversion live - each content chunk arrives as its own \\`session/update\\` notification (same \\`messageId\\`), then the \\`PromptResponse\\`. Without it, the whole turn arrives as one buffered \\`session/update\\` notification. Streaming is inherent to ACP; content only ever rides notifications. - A failed conversion streams an explanatory \\`session/update\\`, then ends with \\`stopReason: \"error\"\\`. \\`session/cancel\\` is a client→agent notification - the agent acknowledges it with HTTP 204 and no body. - Token status (\\`X-Token-Balance\\`, \\`X-Token-Expires\\`, \\`X-Token-Status\\`) is proxied from the core into the response headers. ## Usage scenarios mdapi.io is a minimal, self-documenting service-transport primitive: REST, MCP, ACP, A2A, and OpenAI-compatible endpoints all call the same transformation core, so agents can combine protocols and pass already-processed knowledge between each other. - \\*\\*Agent swarms\\*\\* - each request is handled by a stateless, automatically-scaled execution environment, so the service scales horizontally. An orchestrator fans work out across a swarm of agents, and the swarm processes very large batches of distinct resources in parallel - millions of resources in a matter of minutes, the ceiling set by how widely the work is distributed rather than by the service. Different users may freely access the same resource. - \\*\\*Shared vs individual payment\\*\\* - an orchestrator can pay once for a shared token (batching on-chain activity), or each agent can activate its own token for the exact volume it received. - \\*\\*Human-in-the-loop\\*\\* - if an agent has no wallet or insufficient funds, it returns payment details + a QR code; the human pays from a mobile device and the agent resumes. - \\*\\*Role switching\\*\\* - an agent's role can change mid-task; one agent fetches/normalizes, another summarizes/extracts, relaying compact results via the text or prompt parameters. - \\*\\*Cross-protocol interoperability\\*\\* - knowledge extracted on one protocol is reusable on another. - \\*\\*Bulk processing / model training\\*\\* - the swarm pattern turns mdapi.io into a high-throughput edge pipeline for large corpora. See https://mdapi.io/about for the full scenario walkthrough. ## Links - \\*\\*About service:\\*\\* https://mdapi.io/about - \\*\\*API docs:\\*\\* https://mdapi.io - \\*\\*MCP server manifest:\\*\\* https://mdapi.io/mcp - \\*\\*Health check:\\*\\* https://mdapi.io/health - \\*\\*API documentation:\\*\\* https://mdapi.io/llms.txt - \\*\\*Full API documentation:\\*\\* https://mdapi.io/llms-full.txt - \\*\\*AI discovery:\\*\\* https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json - \\*\\*AI Agent discovery:\\*\\* https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json - \\*\\*A2A Agent card:\\*\\* https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json - \\*\\*ACP manifest:\\*\\* https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json - \\*\\*x402 payment manifest:\\*\\* https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json - \\*\\*OpenAPI specification (JSON):\\*\\* https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json - \\*\\*OpenAPI specification (YAML):\\*\\* https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml - \\*\\*MAPI specification (case-insensitive path MAPI.md support):\\*\\* https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md - \\*\\*Skill specification (case-insensitive path SKILL.md support):\\*\\* https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md - \\*\\*Agent Plugins package (agent-plugins.org v1.0.0):\\*\\* https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/ ## External Links - \\*\\*github.com\\*\\* https://github.com/mdapiio/mdapi.io - \\*\\*skills.sh\\*\\* https://www.skills.sh/mdapiio/mdapi.io - \\*\\*skillsmp.com\\*\\* https://skillsmp.com/creators/mdapiio/mdapi.io - \\*\\*clawhub.ai\\*\\* https://clawhub.ai/mdapiio - \\*\\*x.com\\*\\* https://x.com/mdapiio ## Disclaimer \\*\\*The service is provided \"AS IS\".\\*\\* > mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.\n",
"markdownStats": {
"images": 0,
"links": 0,
"tables": 0,
"codeBlocks": 0,
"headings": 0
},
"tokens": {
"htmlTokens": 13428,
"markdownTokens": 13397,
"reduction": 31,
"reductionPercent": 0
},
"score": {
"score": 45,
"grade": "D",
"rubricVersion": 3,
"dimensions": {
"accessibility": {
"score": 75,
"weight": 30,
"grade": "B",
"checks": {
"content_without_js": {
"score": 100,
"weight": 55,
"evidence": "proven",
"details": "Content available without JavaScript"
},
"fast_content_position": {
"score": 0,
"weight": 25,
"evidence": "plausible",
"details": "Main content starts at 100% of HTML"
},
"reasonable_page_size": {
"score": 100,
"weight": 20,
"evidence": "plausible",
"details": "Page size: 58KB"
}
}
},
"aiDiscoverability": {
"score": 46,
"weight": 25,
"grade": "D",
"checks": {
"robots_allows_ai_bots": {
"score": 0,
"weight": 35,
"evidence": "proven",
"details": "5/5 AI search bots blocked: OAI-SearchBot, Claude-SearchBot, PerplexityBot, DuckAssistBot, Amazonbot"
},
"supports_markdown_negotiation": {
"score": 25,
"weight": 20,
"evidence": "plausible",
"details": "CDN level (Cloudflare) — Content negotiation",
"mechanisms": {
"negotiation": true,
"mdUrl": false,
"linkTag": false,
"linkHeader": false
}
},
"has_sitemap": {
"score": 100,
"weight": 15,
"evidence": "plausible",
"details": "Sitemap found"
},
"has_robots_txt": {
"score": 100,
"weight": 10,
"evidence": "plausible",
"details": "robots.txt exists"
},
"has_llms_txt": {
"score": 100,
"weight": 10,
"evidence": "speculative",
"details": "llms.txt exists and is valid"
},
"has_content_signals": {
"score": 60,
"weight": 10,
"evidence": "speculative",
"details": "robots.txt: search=yes, ai-train=no, ai-input=no",
"mechanisms": {
"robotsTxt": true,
"httpHeader": false,
"policy": false
}
}
}
},
"structuredData": {
"score": 0,
"weight": 20,
"grade": "F",
"checks": {
"has_schema_org": {
"score": 0,
"weight": 35,
"evidence": "proven",
"details": "No JSON-LD / Schema.org found"
},
"has_open_graph": {
"score": 0,
"weight": 20,
"evidence": "plausible",
"details": "0/3 OG tags present"
},
"has_meta_description": {
"score": 0,
"weight": 20,
"evidence": "plausible",
"details": "No meta description"
},
"has_canonical_url": {
"score": 0,
"weight": 15,
"evidence": "plausible",
"details": "No canonical URL"
},
"has_lang_attribute": {
"score": 0,
"weight": 10,
"evidence": "plausible",
"details": "No lang attribute on <html>"
}
}
},
"semanticHtml": {
"score": 35,
"weight": 15,
"grade": "F",
"checks": {
"proper_heading_hierarchy": {
"score": 0,
"weight": 30,
"evidence": "plausible",
"details": "No headings found"
},
"uses_article_or_main": {
"score": 0,
"weight": 25,
"evidence": "plausible",
"details": "Missing <article> and <main> elements"
},
"semantic_elements": {
"score": 50,
"weight": 20,
"evidence": "plausible",
"details": "No divs or semantic elements found"
},
"meaningful_alt_texts": {
"score": 100,
"weight": 15,
"evidence": "plausible",
"details": "No images found"
},
"low_div_nesting": {
"score": 100,
"weight": 10,
"evidence": "speculative",
"details": "No divs found"
}
}
},
"contentEfficiency": {
"score": 60,
"weight": 10,
"grade": "C",
"checks": {
"token_reduction_ratio": {
"score": 0,
"weight": 40,
"evidence": "speculative",
"details": "0% token reduction (HTML→Markdown)"
},
"content_to_noise_ratio": {
"score": 100,
"weight": 30,
"evidence": "speculative",
"details": "Content ratio: 99.5% (59274 content chars / 59583 HTML bytes)"
},
"reasonable_page_weight": {
"score": 100,
"weight": 20,
"evidence": "speculative",
"details": "HTML size: 58KB"
},
"minimal_inline_styles": {
"score": 100,
"weight": 10,
"evidence": "speculative",
"details": "0/28 elements with inline styles (0.0%)"
}
}
}
}
},
"recommendations": [
{
"id": "allow_ai_bots",
"priority": "critical",
"category": "aiDiscoverability",
"titleKey": "rec.allow_ai_bots.title",
"descriptionKey": "rec.allow_ai_bots.description",
"howToKey": "rec.allow_ai_bots.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 8.8,
"maxImpact": 8.8,
"evidence": "proven",
"checkScore": 0,
"checkDetails": "5/5 AI search bots blocked: OAI-SearchBot, Claude-SearchBot, PerplexityBot, DuckAssistBot, Amazonbot"
},
{
"id": "move_content_earlier",
"priority": "critical",
"category": "accessibility",
"titleKey": "rec.move_content_earlier.title",
"descriptionKey": "rec.move_content_earlier.description",
"howToKey": "rec.move_content_earlier.howto",
"howToStepKeys": null,
"effort": "moderate",
"estimatedImpact": 7.5,
"maxImpact": 7.5,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "Main content starts at 100% of HTML"
},
{
"id": "add_schema_org",
"priority": "critical",
"category": "structuredData",
"titleKey": "rec.add_schema_org.title",
"descriptionKey": "rec.add_schema_org.description",
"howToKey": "rec.add_schema_org.howto",
"howToStepKeys": null,
"effort": "moderate",
"estimatedImpact": 7,
"maxImpact": 7,
"evidence": "proven",
"checkScore": 0,
"checkDetails": "No JSON-LD / Schema.org found"
},
{
"id": "fix_heading_hierarchy",
"priority": "high",
"category": "semanticHtml",
"titleKey": "rec.fix_heading_hierarchy.title",
"descriptionKey": "rec.fix_heading_hierarchy.description",
"howToKey": "rec.fix_heading_hierarchy.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 4.5,
"maxImpact": 4.5,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "No headings found"
},
{
"id": "add_open_graph",
"priority": "high",
"category": "structuredData",
"titleKey": "rec.add_open_graph.title",
"descriptionKey": "rec.add_open_graph.description",
"howToKey": "rec.add_open_graph.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 4,
"maxImpact": 4,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "0/3 OG tags present"
},
{
"id": "add_meta_description",
"priority": "high",
"category": "structuredData",
"titleKey": "rec.add_meta_description.title",
"descriptionKey": "rec.add_meta_description.description",
"howToKey": "rec.add_meta_description.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 4,
"maxImpact": 4,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "No meta description"
},
{
"id": "improve_token_efficiency",
"priority": "high",
"category": "contentEfficiency",
"titleKey": "rec.improve_token_efficiency.title",
"descriptionKey": "rec.improve_token_efficiency.description",
"howToKey": "rec.improve_token_efficiency.howto",
"howToStepKeys": null,
"effort": "significant",
"estimatedImpact": 4,
"maxImpact": 4,
"evidence": "speculative",
"checkScore": 0,
"checkDetails": "0% token reduction (HTML→Markdown)"
},
{
"id": "add_article_main",
"priority": "high",
"category": "semanticHtml",
"titleKey": "rec.add_article_main.title",
"descriptionKey": "rec.add_article_main.description",
"howToKey": "rec.add_article_main.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 3.8,
"maxImpact": 3.8,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "Missing <article> and <main> elements"
},
{
"id": "add_markdown_negotiation",
"priority": "high",
"category": "aiDiscoverability",
"titleKey": "rec.add_markdown_negotiation.title",
"descriptionKey": "rec.add_markdown_negotiation.description_partial",
"howToKey": "rec.add_markdown_negotiation.howto",
"howToStepKeys": [
"rec.add_markdown_negotiation.step.md_url",
"rec.add_markdown_negotiation.step.link_tag",
"rec.add_markdown_negotiation.step.link_header"
],
"effort": "significant",
"estimatedImpact": 3.8,
"maxImpact": 5,
"evidence": "plausible",
"checkScore": 25,
"checkDetails": "CDN level (Cloudflare) — Content negotiation"
},
{
"id": "add_canonical_url",
"priority": "high",
"category": "structuredData",
"titleKey": "rec.add_canonical_url.title",
"descriptionKey": "rec.add_canonical_url.description",
"howToKey": "rec.add_canonical_url.howto",
"howToStepKeys": null,
"effort": "quick-win",
"estimatedImpact": 3,
"maxImpact": 3,
"evidence": "plausible",
"checkScore": 0,
"checkDetails": "No canonical URL"
}
],
"llmsTxtPreview": "# mdapi.io\n\n## Main\n- [Home](https://mdapi.io)\n\n",
"llmsTxtExisting": "# mdapi.io - Minimal Data API I/O: a content transformation layer primitive for AI systems.\n\n\nTransforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage.\n\n## Agent entrypoint\n\n- **Start AI discovery** → https://mdapi.io/.well-known/ai-discovery.json\n- **Use skill** → https://mdapi.io/.well-known/skill.md\n\n## Quick Start\n\nChoose your entry point based on your role:\n\n| Role | Protocol | Endpoint | When to use |\n| ----------------------------------------------------- | ---------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| IDE / coding agent (JetBrains, Cursor, VS Code, etc.) | ACP (Agent Client Protocol) | POST /acp | You are an IDE plugin or coding agent. Use initialize + session/new + session/prompt (content arrives via session/update notifications). |\n| AI agent (Claude Code, Codex, OpenClaw, Hermes, etc.) | A2A (Agent-to-Agent) | POST /a2a | You are an autonomous agent. Use SendMessage with data in text parts. Supports streaming and task tracking. |\n| AI agent (any framework) | MCP (Model Context Protocol) | GET /mcp + POST /mcp | You need tool discovery. Use tools/call with convert tool. |\n| OpenAI-compatible client | OpenAI API | POST /v1/chat/completions | You already use OpenAI SDK. Pass URL/file in messages. Supports streaming. |\n| Direct HTTP / curl / script | REST API | GET / or POST / | Simplest path. GET returns Markdown directly. POST returns JSON with metadata. |\n\n### Universal discovery\n\nAll protocols and capabilities are described in one file:\nGET /.well-known/ai-discovery.json\n\n## Features\n\n- Stateless, in-memory processing\n- Edge execution with automatic scaling\n- Prompt-driven transformation\n- AI-optimized output for LLMs\n- Pay-per-use via x402 v1/v2 or manual payment\n\n## Supported Formats\n\n| Type | Formats |\n| --------- | ------------------------------ |\n| Documents | PDF, DOCX, XLSX, XLS, ODT, ODS |\n| Images | JPEG, JPG, PNG, WebP, SVG |\n| Text | HTML, XML, JSON, CSV, TXT |\n| Webpages | Any publicly accessible URL |\n\n## Limits\n\n- Max file size: 50 MB\n- Max URL content: 50 MB\n- Rate limit: 10,000 requests per hour\n- Free tier: 10 requests per day (no token required), within the service’s overall free quota\n- Paid tier: min $0.01 per conversion (USDC on Solana)\n- Token validity: 1 year\n\n## Authentication\n\n- Bearer: Authorization: Bearer TOKEN\n- Header: X-Token-Required: TOKEN\n\n## Token Activation\n\nBefore using a paid token, it must be activated:\n1. Receive 402 with NEW token+memo (pending created)\n2. Send USDC on Solana to wallet with memo from 402\n3. Retry request with EXACT token+memo from 402\n4. After activation: use token only\n\nImportant: The token+memo in 402 response MUST be used - old token or memo will be rejected.\n\n## Method Semantics\n\n| Method | Response Format | Notes |\n| ------ | --------------- | ----------------------------------------- |\n| GET | Markdown | Always returns Markdown, including errors |\n| POST | JSON | Always returns JSON, including errors |\n| Error | Same as method | GET errors = Markdown, POST errors = JSON |\n\n## GET / (Conversion)\n\nParameters: input, prompt, result, stream, token, memo\n- input: Auto-detected: starts with \"http(s)://\" → URL mode (fetched), starts with \"data:\" → file mode (data URI decoded), otherwise → text mode (processed as-is).\n- ⚠️ Browser URL limit: ~2048 chars. Use POST for long text/prompt values.\n- prompt: Custom LLM instructions\n- result: markdown|prompt|both (default: markdown)\n- stream: Enable streaming (boolean, default false) for SSE response\n- token: Access token\n- memo: Token activation memo\n\n## POST / (JSON body)\n\nReturns JSON with metadata. Accepts JSON body with \"input\" parameter.\nAuto-detection: starts with \"http://\" → URL, starts with \"data:\" → file, otherwise → text.\n\n## stream parameter\n\n- true (boolean): Returns SSE stream with real-time chunks\n- Token info sent as first message: {\"type\":\"token_info\",...}\n- Content sent as OpenAI-compatible chunks: {\"choices\":[{\"delta\":{\"content\":\"...\"},\"finish_reason\":null}]}\n- Final chunk: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n- End marker: data: [DONE]\n- Native framing per protocol: MCP streams notifications/message chunks, ACP streams session/update notification chunks, A2A streams result.task → artifactUpdate → statusUpdate frames (REST/OpenAI use the choices/delta format above)\n\n## result parameter\n\n- markdown: raw converted Markdown (default)\n- prompt: LLM result only (requires prompt)\n- both: markdown + prompt_result\n\n## prompt examples\n\n| Use Case | Example |\n| ------------------ | ---------------------------------------------------------- |\n| Summarize | `?input=https://example.com&prompt=Summarize` |\n| Extract key points | `?input=Hello World&prompt=Extract key points` |\n| Convert to JSON | `?input=https://example.com&prompt=Convert to JSON format` |\n| Analyze content | `?input=Hello World&prompt=Analyze and explain` |\n\n## Response (POST)\n\n{\n \"success\": true,\n \"markdown\": \"# Converted content...\",\n \"prompt_result\": \"... (when result=prompt/both)\",\n \"token_status\": \"free|valid|invalid|expired|exhausted|expired_pending|activated|verification_error|invalid_payment|error|pending\",\n \"token_balance\": 0.99,\n \"token_expires\": 1798761600\n}\n\n## Error Codes\n\n| HTTP | code (stable) | Description |\n| ---- | -------------------------------------------- | ------------------------------------------------ |\n| 200 | - | Success |\n| 400 | invalid_request / invalid_input | Bad request |\n| 401 | unauthorized / invalid_token / token_expired | Invalid token |\n| 402 | payment_required | Payment required |\n| 403 | blocked | Blocked |\n| 404 | not_found | Not found |\n| 413 | too_large / context_overflow | File too large or exceeds the LLM context window |\n| 415 | unsupported_format | Format not supported |\n| 429 | rate_limited / resource_limit | Rate limit exceeded |\n| 500 | server_error | Internal error |\n| 503 | ai_unavailable | AI service unavailable |\n| 504 | timeout | Timeout |\n\n## Endpoints\n\n| Method | Path | Description |\n| ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------- |\n| GET | /about | About service |\n| GET | / | API docs or conversion (via query parameter: `input`, `prompt`, `result`) |\n| POST | / | Convert content via JSON body (supports `input`, `prompt`, `result` params) |\n| POST | /v1/chat/completions | OpenAI-compatible endpoint |\n| GET | /mcp | MCP server manifest |\n| POST | /mcp | MCP RPC endpoint (discover, tools, resources, subscriptions) |\n| POST | /acp | ACP RPC endpoint (IDE agents) |\n| POST | /a2a | A2A RPC endpoint (agent2agent) |\n| GET | /health | Health check |\n| GET | /llms.txt | API documentation |\n| GET | /llms-full.txt | Full API documentation |\n| GET | /.well-known/ai-discovery.json or /ai-discovery.json | AI discovery |\n| GET | /.well-known/agent.json or /agent.json | AI Agent discovery |\n| GET | /.well-known/agent-card.json or /agent-card.json | A2A Agent card |\n| GET | /.well-known/acp.json or /acp.json | ACP manifest |\n| GET | /.well-known/x402.json or /x402.json | x402 payment manifest |\n| GET | /.well-known/openapi.json or /openapi.json | OpenAPI specification (JSON) |\n| GET | /.well-known/openapi.yaml or /openapi.yaml | OpenAPI specification (YAML) |\n| GET | /.well-known/mapi.md or /mapi.md | MAPI specification (case-insensitive path MAPI.md support) |\n| GET | /.well-known/skill.md or /skill.md | Skill specification (case-insensitive path SKILL.md support) |\n| GET | /.well-known/skills/index.json | Legacy skills index |\n| GET | /.well-known/agent-skills/index.json | Agent Skills discovery (v0.2.0) |\n| GET | /.well-known/api-catalog | API catalog (linkset+json, RFC draft) |\n| GET | /.well-known/mcp/server-cards.json | MCP server cards index |\n| GET | /.well-known/plugin/plugin.json or /.well-known/plugin.json | Agent Plugins v1.0.0 manifest |\n| GET | /.well-known/plugin/mcp.json | Agent Plugins MCP config |\n| GET | /.well-known/plugin/skills/mdapi-conversion/SKILL.md | Agent Plugins conversion skill |\n\n## Links\n\n- **About service:** https://mdapi.io/about\n- **API docs:** https://mdapi.io\n- **MCP server manifest:** https://mdapi.io/mcp\n- **Health check:** https://mdapi.io/health\n- **API documentation:** https://mdapi.io/llms.txt\n- **Full API documentation:** https://mdapi.io/llms-full.txt\n- **AI discovery:** https://mdapi.io/.well-known/ai-discovery.json or https://mdapi.io/ai-discovery.json\n- **AI Agent discovery:** https://mdapi.io/.well-known/agent.json or https://mdapi.io/agent.json\n- **A2A Agent card:** https://mdapi.io/.well-known/agent-card.json or https://mdapi.io/agent-card.json\n- **ACP manifest:** https://mdapi.io/.well-known/acp.json or https://mdapi.io/acp.json\n- **x402 payment manifest:** https://mdapi.io/.well-known/x402.json or https://mdapi.io/x402.json\n- **OpenAPI specification (JSON):** https://mdapi.io/.well-known/openapi.json or https://mdapi.io/openapi.json\n- **OpenAPI specification (YAML):** https://mdapi.io/.well-known/openapi.yaml or https://mdapi.io/openapi.yaml\n- **MAPI specification (case-insensitive path MAPI.md support):** https://mdapi.io/.well-known/mapi.md or https://mdapi.io/mapi.md\n- **Skill specification (case-insensitive path SKILL.md support):** https://mdapi.io/.well-known/skill.md or https://mdapi.io/skill.md\n- **Agent Plugins package (agent-plugins.org v1.0.0):** https://mdapi.io/.well-known/plugin.json - portable manifest (plugin.json) + mcp.json + skills/mdapi-conversion/SKILL.md under https://mdapi.io/.well-known/plugin/\n\n## External Links\n\n- **github.com** https://github.com/mdapiio/mdapi.io\n- **skills.sh** https://www.skills.sh/mdapiio/mdapi.io\n- **skillsmp.com** https://skillsmp.com/creators/mdapiio/mdapi.io\n- **clawhub.ai** https://clawhub.ai/mdapiio\n- **x.com** https://x.com/mdapiio\n\n## Disclaimer\n\n**The service is provided \"AS IS\".**\n\n\n> mdapi.io is an edge-native service-transport primitive for AI, autonomous-agents, and the Web4 ecosystem.",
"emergingProtocols": {
"oauthProtectedResource": {
"exists": false,
"url": "https://mdapi.io/.well-known/oauth-protected-resource"
},
"oauthDiscovery": {
"exists": false,
"url": "https://mdapi.io/.well-known/oauth-authorization-server"
},
"mcpServerCard": {
"exists": true,
"url": "https://mdapi.io/.well-known/mcp/server-card.json",
"draft": true,
"name": null,
"version": "1.0.0",
"description": "Model Context Protocol server manifest for mdapi.io",
"transport": null,
"tools": 1,
"resources": 15,
"prompts": null
},
"a2aAgentCard": {
"exists": true,
"url": "https://mdapi.io/.well-known/agent-card.json",
"name": "mdapi",
"version": "1.0.0",
"description": "Minimal Data API I/O: a content transformation layer primitive for AI systems. Transforms documents, images, and webpages into AI-ready Markdown and structured data, optimized for LLM efficiency and token usage.",
"capabilities": 3,
"skills": 7,
"endpoint": null
},
"apiCatalog": {
"exists": true,
"url": "https://mdapi.io/.well-known/api-catalog",
"contentType": "application/linkset+json; charset=utf-8",
"validMediaType": true,
"apis": 1
},
"agentSkills": {
"exists": true,
"url": "https://mdapi.io/.well-known/agent-skills/index.json",
"draft": true,
"schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": 1,
"names": [
"mdapi-conversion"
]
},
"count": 4,
"total": 6
},
"botAccess": {
"probed": true,
"bot": "OAI-SearchBot",
"controlStatus": 200,
"botStatus": 200,
"discriminates": false,
"refusedAsBot": false,
"edge": "Cloudflare",
"verifiable": false,
"detail": "This origin answers OAI-SearchBot exactly as it answers any other client (200). No edge-level filtering of AI crawlers observed."
},
"snippets": [
{
"id": "fix_heading_hierarchy",
"title": "Fix heading hierarchy",
"description": "Your page has no <h1>. Every page needs exactly one <h1> as the main heading. Add it inside your <main> or <article>.",
"language": "html",
"code": "<h1>Your Page Title</h1>",
"filename": "<main> or <article>"
},
{
"id": "add_open_graph",
"title": "Add missing Open Graph tags",
"description": "Open Graph tags control how your page looks when shared on social media and how AI platforms preview your URL in answers.",
"language": "html",
"code": "<meta property=\"og:title\" content=\"Page Title\">\n<meta property=\"og:description\" content=\"Page description.\">\n<meta property=\"og:image\" content=\"https://yoursite.com/og-image.jpg\">\n<meta property=\"og:url\" content=\"https://mdapi.io\">\n<meta property=\"og:type\" content=\"website\">",
"filename": "<head>",
"stacks": [
{
"id": "html",
"label": "HTML <head>",
"language": "html",
"filename": "<head>",
"code": "<meta property=\"og:title\" content=\"Page Title\">\n<meta property=\"og:description\" content=\"Page description.\">\n<meta property=\"og:image\" content=\"https://yoursite.com/og-image.jpg\">\n<meta property=\"og:url\" content=\"https://mdapi.io\">\n<meta property=\"og:type\" content=\"website\">"
},
{
"id": "wordpress",
"label": "WordPress",
"language": "php",
"filename": "functions.php",
"code": "<?php\n// Quick Open Graph tags without a plugin (skip if Yoast / Rank Math is active)\nadd_action('wp_head', function () {\n if (!is_singular()) return;\n $post = get_queried_object();\n $title = get_the_title($post);\n $desc = get_the_excerpt($post) ?: wp_trim_words(strip_tags($post->post_content), 30);\n $image = get_the_post_thumbnail_url($post, 'large') ?: 'https://yoursite.com/og-image.jpg';\n $url = get_permalink($post);\n printf('<meta property=\"og:title\" content=\"%s\">' . \"\\n\", esc_attr($title));\n printf('<meta property=\"og:description\" content=\"%s\">' . \"\\n\", esc_attr($desc));\n printf('<meta property=\"og:image\" content=\"%s\">' . \"\\n\", esc_url($image));\n printf('<meta property=\"og:url\" content=\"%s\">' . \"\\n\", esc_url($url));\n echo '<meta property=\"og:type\" content=\"article\">' . \"\\n\";\n}, 5);"
},
{
"id": "nextjs",
"label": "Next.js",
"language": "typescript",
"filename": "app/page.tsx",
"code": "// Next.js App Router — Metadata API\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n title: \"Page Title\",\n description: \"Page description.\",\n openGraph: {\n title: \"Page Title\",\n description: \"Page description.\",\n url: \"https://mdapi.io\",\n images: [\"https://yoursite.com/og-image.jpg\"],\n type: 'website',\n },\n};"
}
]
},
{
"id": "add_meta_description",
"title": "Add meta description",
"description": "A good meta description (50-160 characters) helps AI agents understand your page quickly.",
"language": "html",
"code": "<meta name=\"description\" content=\"A brief description of this page.\">",
"filename": "<head>"
},
{
"id": "add_canonical_url",
"title": "Add canonical URL",
"description": "The canonical URL tells AI agents which version of the page is the \"official\" one, avoiding duplicate content issues.",
"language": "html",
"code": "<link rel=\"canonical\" href=\"https://mdapi.io\">",
"filename": "<head>"
},
{
"id": "add_schema_org",
"title": "Add Schema.org JSON-LD",
"description": "Structured data helps AI agents understand the type, author, and purpose of your content.",
"language": "html",
"code": "<script type=\"application/ld+json\">\n{\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebPage\",\n \"name\": \"Page Title\",\n \"description\": \"Page description.\",\n \"url\": \"https://mdapi.io\",\n \"inLanguage\": \"en\"\n}\n</script>",
"filename": "<head>"
},
{
"id": "allow_ai_bots",
"title": "Allow AI bots in robots.txt",
"description": "Some AI bots are blocked in your robots.txt. Remove or modify these Disallow rules to let AI agents index your content.",
"language": "txt",
"code": "# Add to your robots.txt — explicitly allow AI bots:\nUser-agent: GPTBot\nAllow: /\n\nUser-agent: ClaudeBot\nAllow: /\n\nUser-agent: PerplexityBot\nAllow: /",
"filename": "/robots.txt"
},
{
"id": "add_article_main",
"title": "Wrap content in <main> and <article>",
"description": "Semantic HTML landmarks help AI agents identify the main content of your page.",
"language": "html",
"code": "<main>\n <article>\n <h1>Your Page Title</h1>\n <p>Your content here...</p>\n </article>\n</main>",
"filename": "<body>"
},
{
"id": "add_markdown_negotiation",
"title": "Support Markdown for Agents",
"description": "Let AI agents request a clean Markdown version of any page via content negotiation, .md alternate URLs, link tags or Link headers.",
"language": "html",
"code": "<!-- Mechanism 3: link tag advertising the .md alternate -->\n<link rel=\"alternate\" type=\"text/markdown\" href=\"/page.md\">",
"filename": "<head>",
"stacks": [
{
"id": "html",
"label": "HTML <head>",
"language": "html",
"filename": "<head>",
"code": "<!-- Mechanism 3: link tag advertising the .md alternate -->\n<link rel=\"alternate\" type=\"text/markdown\" href=\"/page.md\">"
},
{
"id": "express",
"label": "Express",
"language": "javascript",
"filename": "server.js",
"code": "// Mechanisms 1 + 4: content negotiation + Link header\napp.get('/page', (req, res) => {\n res.setHeader('Vary', 'Accept');\n res.setHeader('Link', '</page.md>; rel=\"alternate\"; type=\"text/markdown\"');\n if ((req.headers.accept || '').includes('text/markdown')) {\n res.type('text/markdown; charset=utf-8');\n return res.send(renderMarkdown('page'));\n }\n res.render('page');\n});"
},
{
"id": "fastify",
"label": "Fastify",
"language": "javascript",
"filename": "server.js",
"code": "// Mechanisms 1 + 4: content negotiation + Link header\nfastify.get('/page', async (req, reply) => {\n reply.header('Vary', 'Accept');\n reply.header('Link', '</page.md>; rel=\"alternate\"; type=\"text/markdown\"');\n if ((req.headers.accept || '').includes('text/markdown')) {\n return reply.type('text/markdown; charset=utf-8').send(renderMarkdown('page'));\n }\n return reply.view('/page.ejs');\n});"
},
{
"id": "nextjs",
"label": "Next.js",
"language": "typescript",
"filename": "app/page/route.ts",
"code": "// Next.js App Router — Route Handler returning Markdown\nimport { NextRequest } from 'next/server';\nimport { renderMarkdown } from '@/lib/md';\nexport async function GET(req: NextRequest) {\n const accept = req.headers.get('accept') || '';\n if (accept.includes('text/markdown')) {\n return new Response(await renderMarkdown('page'), {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Vary': 'Accept',\n },\n });\n }\n // Fall through to the page component\n return new Response(null, { status: 404 });\n}"
},
{
"id": "wordpress",
"label": "WordPress",
"language": "php",
"filename": "functions.php",
"code": "<?php\n// Mechanism 1: respond to Accept: text/markdown on the same URL\nadd_action('template_redirect', function () {\n if (!is_singular()) return;\n $accept = $_SERVER['HTTP_ACCEPT'] ?? '';\n if (strpos($accept, 'text/markdown') === false) return;\n header('Content-Type: text/markdown; charset=utf-8');\n header('Vary: Accept');\n $post = get_queried_object();\n echo \"# \" . get_the_title($post) . \"\\n\\n\";\n echo wp_strip_all_tags(apply_filters('the_content', $post->post_content));\n exit;\n});"
},
{
"id": "static",
"label": "Hugo / Jekyll / Astro",
"language": "txt",
"filename": "static/page.md",
"code": "# Mechanism 2: serve .md alongside .html\n# Hugo: place page.md in /static/ — built unchanged\n# Jekyll: drop page.md in /assets/ — copied as-is\n# Astro: src/pages/page.md.ts that exports a GET returning markdown\n\n# Then advertise with mechanism 3 in <head>:\n# <link rel=\"alternate\" type=\"text/markdown\" href=\"/page.md\">"
}
]
}
]
}
API를 사용하여 프로그래밍 방식으로 가져올 수 있습니다 (곧 출시)
이 JSON은 내부용입니다 — Markdown 및 llms.txt 파일과 달리 사이트에 업로드하기 위한 것이 아닙니다. 시간에 따른 점수 추적을 위한 기준값으로 저장하거나, 개발팀과 공유하거나, CI/CD 파이프라인에 통합하세요.
결과 공유
또는 AI에게 개선 방법을 물어보세요
다른 의견도 들어보시겠어요?
Cloudflare에도 무료 스캐너가 있는데, 던지는 질문이 다릅니다. 그쪽은 에이전트가 호출할 수 있도록 사이트가 게시한 것(MCP 서버 카드, Agent Skills, API 카탈로그, DNS 레코드)을 채점합니다. 저희는 에이전트가 콘텐츠에 도달해 읽고 이해할 수 있는지를 채점합니다. 한쪽은 좋고 다른 쪽은 나쁠 수 있으니 두 점수가 다른 것은 당연합니다. 서로 다른 질문에 대한 답이고, 둘 다 알아둘 가치가 있습니다.
Cloudflare로 mdapi.io 검사하기배지 삽입
이 배지를 사이트에 추가하세요. AI 준비도 점수가 변경되면 자동으로 업데이트됩니다.
<script src="https://agentready.md/badge.js" data-id="20ca3000-2a3c-4b2d-86c0-6c897f642cde" data-domain="mdapi.io"></script>
[](https://agentready.md/ko/r/mdapi.io)
곧 출시: 전체 도메인 분석
전체 도메인을 크롤링하고, llms.txt를 생성하고, AI 준비도 점수를 시간에 따라 모니터링하세요. 대기자 명단에 등록하여 알림을 받으세요.
명단에 등록되었습니다! 서비스 출시 시 알려드리겠습니다.