APIドキュメントが存在しない時、エンジニアがAPI本体から情報を得る実例と注意点

Published on: | Last updated:

Mem0の「Gateway」は告知や233ページの公式ドキュメントに記載がないまま公開され、実質的な一次資料はリンクされていないOpenAPI specification(openapi.json)である。

最初にぶつかるのは、読むべきドキュメントが見当たらない、という事実です。some time between the 8th and the 25th of Augustに出たらしい。らしい、という言い方になるのは、発表がないから。ブログも、changelogも、Product Huntも、Hacker Newsも、何もない。で、公式のドキュメントサイトは233ページあるのに、Gatewayという単語が出てこない。

だから手がかりは「動いているもの」しかない。console。dashboard。クライアントがあるなら、話している先がある。そこから辿るしかない、という流れになります。

ドキュメントがないなら、クライアントが呼ぶ先を探す

dashboardのJavaScriptを見てホスト名を拾うと、アプリ本体とは別にgateway-mcp.mem0.aiが見つかり、/openapi.jsonと/docsと/redocが公開状態で返る。

やったことは、かなり素朴です。dashboardが読み込むJavaScriptを開いて、どこにHTTPを投げているかを見る。そこでホスト名が出てくる。見つかったら叩く。終わり。…終わりなんだけど、ここが一番大事。

curl -s https://gateway.mem0.ai/signup \
  | grep -oE 'src="/_next/static/chunks/[^"]+"' \
  | head -20

# then pull the bundles and search them for API hosts
grep -ohE 'https?://[a-z0-9.-]*mem0[a-z0-9.-]*' chunks/*.js | sort -u
# https://gateway-mcp.mem0.ai

ホストが分かったら、次。そこが「どれだけ喋るか」。で、ここが妙に気前がいい。

curl -s -o /dev/null -w "%{http_code}\n" https://gateway-mcp.mem0.ai/openapi.json   # 200
curl -s -o /dev/null -w "%{http_code}\n" https://gateway-mcp.mem0.ai/docs           # 200
curl -s -o /dev/null -w "%{http_code}\n" https://gateway-mcp.mem0.ai/redoc          # 200

OpenAPI specificationがまるごと出てくる。しかもSwaggerとReDocまで。外から普通に見える。リンクはされていないのに。仕様としては、68 endpoints、89 schemas。ここまで揃っていると、マーケ資料より正確に「何を作ったか」が読めてしまう。静かに。

Concept overview: how an unlinked OpenAPI spec becomes the primary documentation
Concept overview: how an unlinked OpenAPI spec becomes the primary documentation

schema(特にenum)から、製品の輪郭が出る

OpenAPIのschemasからenumを抜くと、権限(read/write/execute)やConnectorKind(mcp/openapi)、PlanKey(start/ship/scale/enterprise)など設計上の前提が露出する。

マーケページは「意図」を語る。schemaは「決めたこと」を語る。ここ、温度差がある。で、enumを抜くと、決めたことがそのまま出ます。

curl -s https://gateway-mcp.mem0.ai/openapi.json \
  | jq '.components.schemas | to_entries[]
        | select(.value.enum) | {(.key): .value.enum}'

{ "ActionClass":    ["read", "write", "execute"] }
{ "AuditDecision":  ["allowed", "denied", "error"] }
{ "ConnectorKind":  ["mcp", "openapi"] }
{ "PlanKey":        ["start", "ship", "scale", "enterprise"] }

ActionClassがread / write / executeの3つ。ConnectorKindはmcpかopenapi。つまり「MCP server」につなぐだけじゃなく、plain OpenAPI servicesも対象にしている。「bring your own API」がロードマップじゃなく、少なくとも設計としては現実にある、という読みになります。

PlanKeyがstart / ship / scale / enterprise。製品が自分をlimited previewと呼んでいる文脈と並べると、ちょっと面白い。ここは評価というより、観察。仕様は正直なので。

それと、予想していなかったendpointが1つある。spec内の説明が、こう書いてある。

"Per agent: tools granted vs tools actually used. The 'strip the rest' report."

この一文が、たぶん一番「使える」機能を言い当てている。でもマーケページには出てこない。両方、2回見ても、ない。…ないものはない。

「コンテキストの問題」は、結局ツール定義の量に帰着する

Gatewayの前提は、複数サービス接続でツール定義が膨れ上がることにあり、16 connectorsと(公開されている10件の合計で)352 toolsという規模感が示されている。

このカテゴリの売り文句はcontext。いろんなサービスをつなぐ。すると、agentが答える前に何百ものtool definitionsを抱えることになる。ここが重い。重いのは、想像じゃなくて構造。

だから数を数える。信じるんじゃなくて、数える。ここはOpenAPI specがあるからできる。

connectorsは16。うち、countを出している10個の合計が352 tools。Vantaだけで135。マーケ側は312という例で話を組んでいるけど、実際のカタログの方が大きい。つまり、売るための例より、現物の方が「太い」。こういうズレは、割と示唆的です。

セットアップ:agent作成とgrant、そしてtool_selectorのデフォルト

agentは作成直後はgrants/effective_grantsが空で到達不能がデフォルトであり、grantでtool_selectorを省略するとwildcard「*」になるがpermission必須のためread/write/executeの範囲に必ず制限される。

実際の流れ。Notionをconnectして、agentを1つ作って、readだけをgrantする。まずagent作成。

POST /api/v1/agents
{ "name": "docs-reader" }

201 Created
{
  "id": "01M0YXWJCZT4GB80NZPWZ38M1F",
  "name": "docs-reader",
  "status": "active",
  "grants": [],
  "effective_grants": []
}

ここでgrantsとeffective_grantsが空。新しいagentは、何にも届かない。ゼロ権限。least privilegeの初期値として、これは正しい。口に出して言う価値があるタイプの「当たり前」です。

次にgrant。ここで気になるのがtool_selector。

PUT /api/v1/agents/01M0YXWJCZT4GB80NZPWZ38M1F/grants
{
  "grants": [
    { "connector_id": "01M0YXS38S5W6PN7Y6WX5S4TW3",
      "permission": "read" }
  ]
}

tool_selectorをわざと省略する。デフォルトを見たいから。結果はwildcard。

{
  "tool_selector": "*",
  "permission": "read"
}

least privilegeをうたう製品が「*」をデフォルトにするのは、見た目だけなら印象が悪い。ここ、反射で書くと危ない。というのもpermissionはrequiredで、デフォルトがない。つまりwildcardは必ずActionClassに束縛される。

このケースだと「すべて」=「すべてのread tool」。すべてのtoolではない。意味が違う。違いは、かなり大きい。

connector authorityとagent authorityは分離されている(ここが製品の核)

Notionのtoken自体がwrite可能でも、readのみgrantしたagentのtools/listにはexecute toolが出ず、connector authorityとagent authorityが別物として扱われる。

Notion connectorの背後には28 toolsがある。内訳はreadが14、変更系が14。Notionのconsent screenは「自分の代理で動く」ことを明示していて、つまりtokenはwriteできる。

でも、agentにはreadしかgrantしていない。じゃあ、agentが実際に何を見えるのか。dashboard表示じゃなく、gatewayをprotocolで叩く。

consoleが渡してくる接続行はこう。

claude mcp add --transport http mem0-gateway \
  https://gateway-mcp.mem0.ai/mcp \
  --header "Authorization: Bearer mg_..."

下で動いているraw callはこれ。

POST https://gateway-mcp.mem0.ai/mcp
Authorization: Bearer mg_...

{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }

返ってきたのは19 tools。Notion readが14。そこにGateway自身の5つのmeta toolsが加わる:discover、find_tools、describe_tool、invoke、request_access。execute toolsは1つも出ない。

tokenはwriteできる。agentはできない。同じ接続の鍵を持っていても、agentの権限は別で切られる。connector authorityとagent authorityが分離されている。これが、この製品を一文で言うなら、たぶんそれ。

Core mechanism: separating connector authority from agent authority and gating tools via grants
Core mechanism: separating connector authority from agent authority and gating tools via grants

denyは速い:fail closedとdeny_reason、そしてlatency_ms

tools/callでwrite系を試すとout_of_scopeで拒否され、denyはgateway外に出ないため3〜4ms、allowedは約1秒で、この差の原因はNotion未計測のため推論として扱われる。

書き込みを試す。拒否されるべきなので、拒否されるかを見る。

{ "jsonrpc": "2.0", "id": 20, "method": "tools/call",
  "params": {
    "name": "notion__notion_update_page",
    "arguments": { "page_id": "00000000-0000-0000-0000-000000000000" }
  }
}

返ってきた拒否が、わりと丁寧です。

{
  "jsonrpc": "2.0", "id": 20,
  "result": {
    "content": [{ "type": "text",
      "text": "mem0 gateway denied this call (out_of_scope).
               Call `discover` to see what you are allowed to do." }],
    "isError": true
  }
}

fail closed。理由がout_of_scopeとして機械が分岐できる形で出る。さらに、recoverの手がかりとしてdiscoverを示す。agentが壁に当たったとき、盲目的にリトライしないで、向きを変えられる。

そして、コストはaudit logに出る。ここは意見じゃなくて、数字として置かれる。

GET /api/v1/audit/events?limit=5

tool           action   decision  deny_reason    latency_ms
create_pages   execute  denied    out_of_scope            3
update_page    execute  denied    out_of_scope            4
search         read     allowed   null                 1438
get_teams      read     allowed   null                  876
get_users      read     allowed   null                  860

denyは3msとか4ms。gatewayから外に出ていないから、という説明が自然です。allowedはだいたい1,000ms台。ここで注意が必要なのは、Notion自体を独立に計測していない点。なので「この差はNotionが遅いからだ」と断定はできない。元の記述どおり、推論として扱うべきです。

ただ、gateway側がsingle digit millisecondsで返しているのに対して、通ったcallが千ミリ秒オーダー、という分離は見える。見えるものだけ言う。そこまで。

meta toolsで「定義を全部渡さない」代わりに、待ち時間を払う

Gatewayはdiscover/find_tools等のmeta toolsでツール定義の読み込みを減らすが、warm計測でfind_toolsが4130〜8206msと遅く、discoverは214msで権限内の全能力一覧を返す。

Gatewayの解き方は、5つのmeta toolsに寄せること。最初から352の定義をmodelに詰め込まない。検索して、必要なものだけ引く。token cost trade-offとしては筋がいい。

でも、その場その場の検索は遅い。ここ、体感で効きます。

warmで4回計測した結果がこれ。

find_tools "find a page about branding"   7202 ms
find_tools "list workspace users"         4130 ms
find_tools "read comments on a page"      6939 ms
find_tools "search my notes"              8206 ms
discover                                   214 ms

semantic lookupが4〜8秒。audit log側も同じ数字を出しているので、回線の気分ではない、と言える範囲がある。

対してdiscoverは214msで、agentのcapability listを全文で返す。読みやすいテキストで。

# mem0 gateway - agent: docs-reader

## You can READ
- notion (14 tools): notion__notion_search, notion__notion_fetch, ...

つまり、安いのは「全部のリスト」。高いのは「意味検索で必要分だけ」。tokenを払うか、latencyを払うか。これは合理的な交換です。ただ、landing pageに書かれがちな話ではない。たぶん、だから書かれていない。

あと、find_toolsはgrantを尊重する。「update a page」と聞いても、writeがないagentには、そもそも書き込み系の候補は浮かび上がらない。policyが拒否するものを検索が露出させない。ここは設計として一貫しています。

granted vs used:overprovisioningレポート(そして、なぜ珍しいか)

/api/v1/audit/overprovisioningはagentごとにgranted_tool_countとused_tool_countを突き合わせ、90 seconds程度の実トラフィックでもunused_toolsを具体名で返す。

specで見つけた、あのendpoint。呼ぶ。

90 secondsの実トラフィックで十分だった、という条件は残しておきます。長時間の話ではない。

GET /api/v1/audit/overprovisioning

[
  {
    "agent_name": "docs-reader",
    "granted_tool_count": 14,
    "used_tool_count": 3,
    "unused_tools": [
      "notion__notion_download_attachment",
      "notion__notion_fetch",
      "notion__notion_get_async_task",
      "notion__notion_get_comments",
      "notion__notion_list_favorite_pages",
      "notion__notion_list_private_pages",
      "notion__notion_list_recent_pages",
      "notion__notion_list_shared_pages",
      "notion__notion_query_data_sources",
      "notion__notion_query_meeting_notes",
      "notion__notion_search_agents"
    ]
  }
]

grantedが14。usedが3。unusedが11個、具体名で出る。「strip the rest」そのままです。消すべきものが列挙される。

このアイデア自体は新しくない。クラウド権限の世界では、genreとして10年級です。Amazon(AWS)がDecember 2015に「grantedとlast accessed data」をやった。Googleが2019に追随。Microsoftは2021に、これ系をやる会社を買った。ここは年号だけ、淡々と置けばいい。

ただ、agentの世界ではほとんど見ない。比較対象として挙げられているのは、Docker's gateway、Pomerium、Cloudflareのportals、Descope、Obot、Kong、agentgateway。どれも「grants」と「calls」の両方のログを持っていそうなのに、joinしてレポートにしていない、という指摘になる。

Mem0はそれをやっている。でも言及しない。妙な沈黙です。

鍵を変えずに止める:kill/activateでポリシーを反映する

agents/{id}/killとactivateで同じkeyのままallowed→DENIED(killed)→allowedに切り替わり、deny_reasonでout_of_scopeと区別される。

もう一つの主張は「credentialをrotateしなくてもrevokeできる」。これは検証が簡単です。

call get_users                    allowed    1295 ms
POST /api/v1/agents/{id}/kill     200
call get_users                    DENIED      205 ms  "denied this call (killed)"
POST /api/v1/agents/{id}/activate 200
call get_users                    allowed     824 ms

keyは同じ。再発行されない。policyが変わって、次のcallに反映される。deny_reasonがkilledになるので、out_of_scopeとは別種の「no」だとログが教える。ここ、運用の視点だと地味に効きます。地味だけど。

変えたい点:UI表示の不一致と、wildcardデフォルトの「楽さ」

consoleは28 tools allowedと表示するがgatewayは14を強制しており表示問題であってセキュリティホールではなく、またtool_selectorの「*」デフォルトはpermissionで制限されるがよりタイトな指定が同程度に容易だと良い。

指摘は2つ。大きい方から。

consoleの画面が「28 tools allowed」と報告して、update page toolも含めて列挙する。でもgatewayの実際のenforcementは14。protocol layerで2回確認しているので、これは表示の問題。display problem。security holeではない。

ただ、オペレータがその画面で監査したら、誤った結論に行く。なので直した方がいい。ここは、評価の語気を強めるより、単純に「危ない誤解が起きる」と言えば十分だと思う。

小さい方はwildcardデフォルト。permissionでboundedだから危険ではない。でも「省略したら*」より、「省略しても狭い方が自然」なケースは多い。

今日でも、3つのnamed toolsを指定するgrantはできる。

{ "connector_id": "...", "permission": "read",
  "tool_selector": "notion__notion_search" }

これが「同じくらい簡単」にできると、運用が楽になる。ほんとに、そこだけ。手触りの話です。

5分で試す手順(そして価値が出るのは最後の一手)

gateway.mem0.aiでサインアップし低リスクのserviceをconnectし、readのみgrantしたagentでMCPのtools/listとwrite拒否を確認し、最後に/api/v1/audit/overprovisioningでunused_toolsを確認する。

  1. https://gateway.mem0.ai/signup でサインアップして、低リスクのserviceを1つconnectする。

  2. agentを作り、そのconnectorに対してreadだけgrantする。

  3. keyをprovisionする。plaintextは一度だけ表示される。

  4. MCP clientを https://gateway-mcp.mem0.ai/mcp に向け、bearer tokenとしてkeyを渡す(Authorization: Bearer)。

  5. tools/listを呼んで、返ってくる数と「存在する数」を見比べる。

  6. writeを試す。single digit millisecondsでfailするのを見る。

  7. 実作業を1分ほどやってから、overprovisioning endpointを呼んで「触っていないgrant」がどれかを見る。

価値が出るのは7。そこまでの1〜6は準備。準備は準備です。淡々と。

結論:少数ツールの単発agentなら不要、複数agent×重要系なら監査とレポートが効く

1つのagentが2つのtoolを呼ぶ程度なら環境変数にkeyを入れて済ませればよいが、複数agentがNotionやdatabaseやbilling providerなど重要系に触るならaudit trailとgranted vs usedレポートが長期的に有用である。

最終的な感想は、わりと割り切れています。

agentが1つで、呼ぶtoolが2つ。そういう規模なら、これは午後を使う価値は薄い。keyを環境変数に入れて、普通に進めればいい。

でも、複数agentを回していて、触る先が「本物」のNotion、本物のdatabase、本物のbilling provider、そういう領域なら、audit trailだけでもこの層の意味は出る。さらに、6か月後にgrantが膨らんでいくのを抑えるのは、granted vs usedのusage reportです。ここは、今のところ他で見ない、という位置づけになる。

最後に方法論。今回の中身は、リンクされていないspecを読んで、動いている製品に対して各claimを照合した結果です。こういう習慣は、credentialを渡す前にやる価値がある。sponsoredであろうとなかろうと、そこは別。

Supplement: an audit-focused workflow from spec → verification → least-privilege cleanup
Supplement: an audit-focused workflow from spec → verification → least-privilege cleanup

Resources / References(原文にあるリンクと注記のみ)

Related to this topic:

Comments