Protocol Design
S2SP divides data communication between an AI agent and MCP servers into a control plane (what the LLM sees) and a data plane (what flows between servers or through the agent's SDK without entering the LLM context).
Data Model
S2SP applies to MCP tools that return structured tabular data — like a pandas DataFrame: n rows × w columns. Each row is a data sample with several column fields.
The columns are divided into two groups at call time by the agent:
- Abstract domains (control plane): Columns the LLM needs for reasoning and decision-making. These are returned to the agent.
- Body domains (data plane): Remaining columns. These bypass the LLM — either cached on the resource server (async) or passed through the agent's SDK layer (sync) without entering the LLM context.
Every row gets an auto-generated _row_id (integer index: 0, 1, 2, ...),
used to reconcile abstract and body when merging later.
# A tool returns 30 columns per row. Agent requests only 4: get_alerts(area="CA", abstract_domains="event,severity,urgency,status") # Response (control plane): { "total_rows": 14, "abstract_domains": ["event", "severity", "urgency", "status"], "body_domains": ["description", "instruction", ... 26 more], "abstract": [ {"_row_id": 0, "event": "Wind Advisory", "severity": "Moderate", ...}, {"_row_id": 1, "event": "Flood Watch", "severity": "Minor", ...}, ... ], "resource_url": "http://host:port/s2sp/data/dK7x_..." // async mode only (presigned URL) }
Two Planes
Control Plane
The control plane uses standard MCP tool calls. The agent calls an
@s2sp_resource_tool()-decorated tool with abstract_domains to
specify which columns it needs. The response contains only those columns +
_row_id. The agent uses this lightweight data to filter, reason,
and decide which rows matter — then tells a consumer server what to do.
Data Plane
The data plane carries body domains. It operates differently depending on the mode:
- Async mode — Direct HTTP between servers. Body data is
cached on the resource server. A consumer server fetches it via
a
POSTrequest to the presignedresource_url. The agent has no visibility into this channel. Data flows server-to-server only. - Sync mode — Data channel through the agent. Body data is returned inline alongside the abstract in a single tool response. The body passes through the agent's SDK layer but is not sent to the LLM — only the abstract enters the LLM context. No server-to-server fetch needed.
Async Mode (default)
Body data is cached on the resource server. The response includes
resource_url but no body.
- Agent calls
get_alerts(area="CA", abstract_domains="event,severity", mode="async") - Resource server fetches all data, assigns
_row_idto each row, caches full data - Returns abstract rows +
resource_url(no body) - Agent filters on abstract, selects rows, passes abstract + resource ref to consumer
- Consumer calls
POST /s2sp/data/{resource_url}on resource server with{"row_ids": [...]} - Source returns full data for selected rows. Agent never saw it.
Agent Resource Server Consumer Server | | | | get_alerts(abstract_ | | | domains="event,..") | | |────────────────────────▶| | | | cache full data | | ◀─ abstract + resource | assign _row_id | | _id + _url | | | | | | filter: pick wind rows | | | | | | draw_chart(abstract, | | | resource_url, _url) | | |────────────────────────────────────────────────────▶| | | | | | POST <resource_url> | | |◀─────────────────────────| | | {"row_ids": [0,2,5]} | | | | | |──────────────────────────▶| | | full body data | | | | | ◀─ chart result | merge + chart
Sync Mode
Body data returned inline. No resource_url.
No server-to-server fetch.
- Agent calls
get_alerts(area="CA", abstract_domains="event,severity", mode="sync") - Source returns abstract + body inline (both as lists of dicts with
_row_id) - Agent's SDK layer separates: abstract → LLM, body → data buffer
- Agent filters on abstract, passes matching abstract + body rows to consumer
- Consumer merges by
_row_id, processes data. No HTTP fetch needed.
Agent Resource Server | | | get_alerts(mode="sync", | | abstract_domains="..") | |─────────────────────────────▶| | | | ◀─ abstract + body inline | | (no resource_url) | | | | SDK: abstract → LLM | | body → data buffer | | | | LLM: filter by event | | | | pass abstract + body | | to consumer (no fetch) |
When to Use Each Mode
| Async Mode | Sync Mode | |
|---|---|---|
| Body location | Cached on resource server | Returned inline to agent |
| Data plane | Direct HTTP between servers | Through agent SDK (not LLM) |
| resource_url | Yes | No |
| Server-to-server fetch | Yes (POST /s2sp/data/) |
No |
| Best for | Multi-server pipelines, large data | Single-agent setups, small data |
| Body through agent process | No | Yes (SDK layer, not LLM) |
Data Plane Endpoint
Every S2SP server exposes an HTTP endpoint for data-plane access:
POST <resource_url> Content-Type: application/json { "row_ids": [0, 2, 5], // optional: select rows by _row_id "columns": ["description"] // optional: select body columns } Response: { "body": [ {"_row_id": 0, "description": "North winds 25-35 mph...", ...}, {"_row_id": 2, "description": "Gusts up to 55 mph...", ...}, ... ], "total_rows": 3, "columns_returned": ["_row_id", "description"] }
- If
row_idsis omitted or empty: all cached rows are returned. - If
columnsis omitted or empty: all columns are returned. _row_idis always included in the response for reconciliation.
Security Model
S2SP uses 256-bit cryptographic resource_url values as capability
tokens. Each resource_url is single-use and expires after a 10-minute
TTL. In async mode, only parties who received the resource_url from the
agent (via the control plane) can fetch data from the resource server's data plane.
Three-Way Trust
- The Agent decides which servers exchange data and which
resource_urlto share with which consumer. - The Source caches data and serves it only to requests
with a valid
resource_url. - The Consumer can only access data the agent explicitly
authorized by sharing the
resource_url.
Token Expiry
Cached data and resource_url references are single-use and expire
after a 10-minute TTL. After expiry, the data is removed from the resource server
server's cache and the resource_url becomes invalid.
MCP Compatibility
Both resource and consumer tools are fully backward-compatible with MCP:
Resource Tool (@s2sp_resource_tool())
- Without
abstract_domains: The tool behaves exactly like a standard MCP tool — returns all data aslist[dict]. No caching, noresource_url, no_row_id. - With
abstract_domains: The S2SP layer activates — filters columns, adds_row_id, caches body, returnsresource_url. - Agents that don't know about S2SP simply omit
abstract_domainsand get normal MCP behavior.
Consumer Tool (@s2sp_consumer_tool())
- Appears as a standard MCP tool with four string parameters:
abstract_data,resource_url,body_data,column_mapping. - Any MCP agent can call it — it's just a regular tool that accepts JSON strings. No special S2SP client required.
- The consumer server handles fetching and merging internally — the agent just passes the parameters from the resource tool response.
General
- MCP Inspector: Every S2SP server works with
mcp devfor debugging. All tools, resources, and prompts are visible and callable. - No spec changes: S2SP uses standard MCP tool parameters and JSON responses. No modifications to the MCP protocol are required.
- Claude Desktop / OpenAI / Anthropic: Tested with all major MCP-compatible clients.