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:

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 (default)

Body data is cached on the resource server. The response includes resource_url but no body.

  1. Agent calls get_alerts(area="CA", abstract_domains="event,severity", mode="async")
  2. Resource server fetches all data, assigns _row_id to each row, caches full data
  3. Returns abstract rows + resource_url (no body)
  4. Agent filters on abstract, selects rows, passes abstract + resource ref to consumer
  5. Consumer calls POST /s2sp/data/{resource_url} on resource server with {"row_ids": [...]}
  6. 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.

  1. Agent calls get_alerts(area="CA", abstract_domains="event,severity", mode="sync")
  2. Source returns abstract + body inline (both as lists of dicts with _row_id)
  3. Agent's SDK layer separates: abstract → LLM, body → data buffer
  4. Agent filters on abstract, passes matching abstract + body rows to consumer
  5. 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"]
}

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

  1. The Agent decides which servers exchange data and which resource_url to share with which consumer.
  2. The Source caches data and serves it only to requests with a valid resource_url.
  3. 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())

Consumer Tool (@s2sp_consumer_tool())

General