How calls work
Once connected, your agent sees three tools. Regions and datasets aren't hundreds of tools — they're the tool_id argument to execute_tool, so adding a region never grows your context.
discover_toolsDescribe the data you want in plain language; it returns the tool_ids that fit.inspect_toolWhen you're unsure how to pass parameters, read the full definition and its pricing first.execute_toolRun it. Billing, permissions and rate limits behave exactly as before.
The outer call always has this shape
execute_tool({ tool_id: "tw_query_dataset", arguments: { dataset_id: "pcc-tender", filters: [{ field: "agency", operator: "contains", value: "交通部" }], limit: 10 } })
The outer call takes exactly two keys, tool_id and arguments. Everything else goes inside arguments — flattening parameters into the outer call is the most common mistake.
Where does that dataset_id come from? Every region page in the data catalogue lists them. The split is simple: that side tells you what the data is, this side tells you how to call it.
curl (streamable-http transport)
Step 1: initialize and capture session id from the mcp-session-id response header
curl -i -X POST https://api.twinkleai.tw/mcp/ \ -H "Authorization: Bearer sk-..." \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
Step 2: tools/call
curl -X POST https://api.twinkleai.tw/mcp/ \ -H "Authorization: Bearer sk-..." \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -H "mcp-session-id: <from-step-1>" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{ "name":"execute_tool", "arguments":{ "tool_id":"tw_search_datasets", "arguments":{"query":"空氣品質","limit":5} }}}'
Response uses SSE-style framing — find the last line starting with data: and parse the JSON.
Python (fastmcp)
import asyncio from fastmcp.client import Client from fastmcp.client.auth import BearerAuth async def main(): async with Client( "https://api.twinkleai.tw/mcp/", auth=BearerAuth("sk-..."), ) as client: tools = await client.list_tools() print([t.name for t in tools]) # 外層一定是 execute_tool,tool_id 與 arguments 兩個鍵 r = await client.call_tool( "execute_tool", { "tool_id": "tw_query_dataset", "arguments": { "dataset_id": "pcc-tender", "filters": [ {"field": "agency", "operator": "contains", "value": "交通部"} ], "limit": 10, }, }, ) # response 在 r.content[0].text — JSON string import json print(json.loads(r.content[0].text)) asyncio.run(main())
Tools return CallToolResult.content[0].text as a JSON string. License, citation and other metadata pass through in the _meta field.
Tools reference
| Tool | Arguments | Returns (brief) |
|---|---|---|
| discover_tools | {query, country?, cursor?} | [{tool_id, name, description, access}] |
| inspect_tool | {tool_id} | {input_schema, how_to_call, execution_policy} |
| execute_tool | {tool_id, arguments} | {tool_id, result, _meta.opendata.license} |
These three are everything tools/ returns. Which one fits a given region or dataset is discover_tools' job; inspect_tool hands you that tool's full JSON Schema and its pricing.
Authentication
- Bearer token — header
Authorization: Bearer sk-.... A Twinkle-Hub-issued virtual key, one per user. Get yours from /dashboard. - No OAuth flow — no PKCE / refresh tokens needed; just a long-lived bearer.
- Rate limit — gated by a rolling credit allowance per plan, not a USD budget or per-key RPM/TPM. The numbers for each plan are on the pricing page.
Pricing
Free allowance, rate limits and how the plans differ are all on the pricing page. When the allowance runs out, the gateway rejects calls until it refills, then resumes on its own — nothing to reset.