ADR-032: Response Transformation β Pluggable Adapter Pattern
Metadataβ
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-06 |
| Linear | N/A |
Contextβ
API responses from backends are often:
- Too verbose for LLM context windows
- Structured for machines, not AI agents
- Inconsistent across different API providers
MCP Gateway needs to transform responses into LLM-friendly formats while respecting token budgets.
The Problemβ
"How do we make arbitrary API responses AI-consumable without hardcoding transformations per API?"
Decisionβ
Implement a Response Transformation Engine with pluggable adapters for different response formats.
Architectureβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TransformEngine β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Field β β Truncation β β Pagination β β
β β Selection β β β β β β
β β β β - Max chars β β - Page size β β
β β - Include β β - Ellipsis β β - Cursor β β
β β - Exclude β β β β β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β
β Adapters: β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ β
β β Notion β β Linear β β Capper β β
β β Adapter β β Adapter β β Adapter β β
β β β β β β β β
β β Blocks β β β Issues β β β Token β β
β β Markdown β β Summary β β Budget β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Transform Operationsβ
Field Selectionβ
Extract only relevant fields:
config = TransformConfig(
include_fields=["id", "title", "status", "created_at"],
exclude_fields=["internal_id", "raw_data"],
)
Truncationβ
Limit response size:
config = TransformConfig(
max_chars=4000,
truncation_marker="... [truncated]",
)
Capper (Token Budget)β
Enforce token limits for LLM context:
class CapperAdapter:
def transform(self, response: dict, max_tokens: int) -> dict:
"""Cap response to fit token budget."""
current_tokens = count_tokens(response)
if current_tokens <= max_tokens:
return response
# Progressive summarization
return self._summarize_to_budget(response, max_tokens)
Adaptersβ
Notion Adapterβ
Transforms Notion block responses:
class NotionAdapter:
def transform(self, blocks: list[dict]) -> str:
"""Convert Notion blocks to Markdown."""
markdown = []
for block in blocks:
if block["type"] == "paragraph":
markdown.append(block["paragraph"]["rich_text"][0]["plain_text"])
elif block["type"] == "heading_1":
markdown.append(f"# {block['heading_1']['rich_text'][0]['plain_text']}")
return "\n\n".join(markdown)
Linear Adapterβ
Transforms Linear issue responses:
class LinearAdapter:
def transform(self, issue: dict) -> dict:
"""Extract key issue fields."""
return {
"id": issue["identifier"],
"title": issue["title"],
"status": issue["state"]["name"],
"assignee": issue.get("assignee", {}).get("name"),
"priority": issue["priority"],
}
Configuration Schemaβ
transform:
enabled: true
default_max_tokens: 4000
adapters:
notion:
enabled: true
output_format: markdown
linear:
enabled: true
include_comments: false
capper:
enabled: true
model: gpt-4 # For token counting
Consequencesβ
Positiveβ
- LLM-Friendly β Responses fit context windows
- Extensible β New adapters without engine changes
- Configurable β Per-tool transformation rules
- Consistent β Uniform output format
Negativeβ
- Information Loss β Truncation removes data
- Adapter Maintenance β Each API needs an adapter
- Token Counting β Requires model-specific logic
Referencesβ
Standard Marchemalo: A 40-year veteran architect understands in 30 seconds