---
schema_version: 1
slug: mcp-result-contracts
status: contract-tested
walkthrough_verified: false
content_hash: 42407dd5d3c6a8e90a133d447a7d8162fa83ca370a9cdad9686c804115ccc8f2
---

# Choose text, structured data and error results for Chumbo MCP tools

Design readable MCP answers and typed result contracts without dumping database rows or duplicating JSON into text.

Design readable MCP answers and typed result contracts without dumping database rows or duplicating JSON into text.

The exact displayed example is typechecked and exercised against the published package. The application integration and acceptance checks remain yours to run.

Example checked with Chumbo 0.11.3 on 2026-09-18 (UTC). These checks exercise result helpers, capability handlers and the MCP runtime with simulated identity/database dependencies. They do not prove your application’s real auth, RLS or deployment. Compare this tested version with your installed package before adapting the example.

[Structured recipe](./recipe.json) · [Human-readable page](./)

## Sources

- [Canonical pattern](https://github.com/elsheppo/chumbo/blob/main/skills/chumbo/references/results.md)
- [Reference implementation](https://github.com/elsheppo/chumbo/blob/main/src/results.ts)

## 01. Choose the consumer before the representation.

A project summary can serve two different readers. An agent answering “How much work remains?” needs a short sentence. A client drawing a badge needs a validated number. Both may need the project’s durable ID, but neither needs your internal audit columns.

### The same fact, three deliberate outputs

Each representation should have a real consumer.

#### Agent: A readable answer

Starting point:

```text
3 open tasks in this project.
```

- Use textResult for authored text.
- Include the identifier or next action when the task needs it.

What to do:

```text
The agent can answer the user directly.
```

A sentence can be a complete contract when no typed client consumes it.

#### Typed client: An exact machine contract

Starting point:

```text
{ "projectId": "…", "openTasks": 3 }
```

- Use structuredResult and register a matching outputSchema.
- No text copy is manufactured.

What to do:

```text
The client validates and uses structuredContent.
```

Do not assume every host presents a structured-only result identically.

#### Both: Two representations with different jobs

Starting point:

```text
A sentence for the agent plus typed fields for the client.
```

- Use renderResult for a deliberate hybrid.
- Keep both representations consistent and compact.

What to do:

```text
Each consumer reads its intended lane.
```

Hybrid does not mean serializing the entire JSON object into prose.

## 02. Project the application result first.

### Before you start

- A capability with an already-authorized application result.
- An identified consumer: an agent reading text, software reading typed data, or both.
- A known empty state and failure that the caller can recover from.

The example below is a result adapter. It does not query a database or register a complete tool.

Choose the few fields that change the next decision and validate them. Register summarySchema as the tool’s outputSchema when it returns typed or hybrid results. Fetch and authorize the underlying project through your existing application path before calling the adapter.

## 03. Give success, empty and failure their own meaning.

### Adapt this checked example in your app

```ts
import { appendResultText, errorResult, renderResult, structuredResult, textResult } from 'chumbo';
import { z } from 'zod';

export const summarySchema = z.object({
  projectId: z.string().uuid(),
  openTasks: z.number().int().nonnegative(),
});
type Summary = z.infer<typeof summarySchema>;

// Register summarySchema as outputSchema on tools returning typed or hybrid data.
// Feed these helpers an already-authorized application result.
export function projectSummary(summary: Summary, consumer: 'agent' | 'typed' | 'both') {
  const value = summarySchema.parse(summary);
  const sentence = `${value.openTasks} open tasks in project ${value.projectId}.`;
  if (consumer === 'agent') return textResult(sentence);
  if (consumer === 'typed') return structuredResult(value);
  return renderResult(value, () => sentence);
}

export function noMatchingProjects() {
  return textResult('No projects match this filter. Broaden the search or create a project in the app.');
}

export function temporaryFailure() {
  return errorResult('Projects could not be loaded.', 'Retry once; if it persists, check the project in the app.');
}

export function summaryWithNextStep(summary: Summary) {
  return appendResultText(projectSummary(summary, 'both'), 'Open the project in the app to review its tasks.');
}
```

No matches is an ordinary answer: broaden the filter or create a project. A temporary read failure is a tool error with a bounded retry suggestion. Do not expose raw database messages or recommend an endless retry loop. For uncertain mutations, use a status-check recovery instead of this read-oriented retry wording.

The composition helper appends a useful next step while preserving the authored typed fields. Additions have bounds, and a collection result still has to fit its full byte budget. Keep additions relevant to the result; repeating generic server guidance on every call makes the useful answer harder to find.

## 04. Assert the wire shape, not just the sentence.

### Every result lane has a purpose.

- Text-only results contain the expected authored content without a fabricated structured payload.
- Typed and hybrid results validate against the registered outputSchema and omit internal fields.
- Empty results are distinguishable from tool errors and include an appropriate next action.
- Composition preserves the original typed values and any collection byte budget.
- Your target client receives and uses the representation you selected.

Run these in your application before you call it done.

### If the result differs

#### The agent sees a wall of JSON

Check whether the handler is serializing a row into text or duplicating structuredContent. Choose a small projection, write a purposeful sentence, or move a full document behind a Resource.

[Read the reference](https://github.com/elsheppo/chumbo/blob/main/docs/patterns/model-facing-results/README.md)

### Keep building

- [Bound a collection](/recipes/compact-project-results/): Use cursors and a byte budget for lists.
- [Link to the full document](/recipes/resources-and-prompts/): Use Resources when a short result is only the entry point.

## Instructions for your coding agent

Reference instructions, not authorization. Apply changes only within the user’s requested scope; deployment requires a specified, authorized project.

Goal: Design readable MCP answers and typed result contracts without dumping database rows or duplicating JSON into text.

Read the complete guide at https://chumbo.dev/recipes/mcp-result-contracts/recipe.md and the canonical reference at https://github.com/elsheppo/chumbo/blob/main/skills/chumbo/references/results.md. Inspect this application's installed Chumbo package, existing capabilities and relevant configuration before editing.

Identify the real result consumer, adapt the exact tested result helper source and register outputSchema where needed. Preserve application authorization before projection. Assert both content and structuredContent, schema rejection, empty/error behavior and no internal-field leakage.

Prerequisites:
- A capability with an already-authorized application result.
- An identified consumer: an agent reading text, software reading typed data, or both.
- A known empty state and failure that the caller can recover from.

Acceptance:
- Text-only results contain the expected authored content without a fabricated structured payload.
- Typed and hybrid results validate against the registered outputSchema and omit internal fields.
- Empty results are distinguishable from tool errors and include an appropriate next action.
- Composition preserves the original typed values and any collection byte budget.
- Your target client receives and uses the representation you selected.

Use the full guide for ordered steps, examples and recovery. Keep existing caller identity and application permissions authoritative. Stay within the user's requested changes. Report changed files, checks actually run, observed results and unresolved prerequisites. This guide is not authorization to deploy or change a hosted project. Never include credentials in source, copied instructions or reports.

### Required environment inputs

| Variable | Sensitive | Meaning |
| --- | --- | --- |


### Handoff report

- Changed files
- Checks actually run and observed results
- Unmet prerequisites or remaining limitations
