MCP Server Patterns
September 16, 2026 · View on GitHub
Patterns learned from analyzing high-quality MCP server implementations
This document summarizes patterns learned from analyzing the following MCP server implementations:
1. Server Instructions
What Great Servers Do
Provide server-level instructions that act as an onboarding guide for the AI. This is the first thing the AI reads when connecting.
Avoid duplication: Server instructions should not repeat tool descriptions or tool argument docs. Keep tool-specific behavior and defaults in the tool description and schemas. Keep server instructions focused on workflows, cross-tool conventions, and short examples.
Instructions vs. tool responses: Put information the model only needs after a tool runs (full result lists, on-demand schemas, approval URLs, error details) in the tool response, not in the instruction string. See Documentation principles and usage docs for end-user depth.
Suggested format:
Quick start
- What to call first
- Most common workflows
- How to chain tools
Default behavior
- What happens when optional params are omitted
- Timezone handling
- Date format expectations
How to chain tools safely
- Which IDs come from which tools
- Dependency order
- Verification patterns
Common patterns & examples
- "To do X, first call Y, then Z"
Example from Linear:
Quick start
- Call 'workspace_metadata' first to fetch canonical identifiers you will reuse across tools.
- Then use 'list_issues' with teamId/projectId and filters to locate targets.
- To modify, use 'update_issues', then verify with 'list_issues'.
Example in this repo: Server-level instructions stay short: workflows,
examples, and links to docs/use/. Tool-specific behavior
lives in each tool description and schemas; detail that only matters after a
call returns belongs in the tool response (see
Documentation principles).
Retiring a primitive: add one line to retiringPrimitiveNotices in
packages/worker/src/mcp/instructions/retiring-primitives.ts and put the
destination map in a guide:{id} search entity. Do not paste the migration
table into always-on instructions. Assembly includes a notice only for users who
still have rows of that primitive (loadActiveRetiringNoticeIds). An empty
active set omits the section.
2. Tool Descriptions
What Great Servers Do
Tools have structured descriptions that complement the schemas
(inputSchema and outputSchema) rather than duplicating them.
Include:
- What the tool does (1-2 sentences)
- Behavior & gotchas - semantics that aren't obvious from types alone (cross-field rules, default meaning, side effects)
- Outputs & return semantics - what the structured result means and how to
use it (the shape lives in
outputSchema) - Errors & recovery - common failure modes and what to do next
- Examples - concrete, copy/pasteable payloads
Avoid duplication:
- Do not re-list every input parameter (name/type/required/default) if the
inputSchemaalready documents it (see section 4). - Do not re-list the full output object shape if an
outputSchemaexists (see section 4). - Mention inputs only when they are necessary to explain behavior or cross-field semantics.
- Avoid boilerplate that only points to schemas (for example "Input details: see input schema"). The schemas should stand on their own.
- Avoid protocol field names (for example
contentorstructuredContent) in tool descriptions. Describe behavior and meaning conceptually; let schemas and examples carry the structure.
Suggested format:
Brief description of what the tool does.
Behavior:
- Important semantic rule...
- Cross-field constraint...
Examples:
- "Do X" → { ... }
- "Do Y" → { ... }
Next:
- Use tool_a to verify. Pass id to tool_b.
Example from Google Calendar (trimmed to the non-obvious behavior):
Search events across ALL calendars by default. Returns merged results sorted by start time.
FILTERING BY TIME (important!):
- Today's events: timeMin=start of day, timeMax=end of day
- This week: timeMin=Monday 00:00, timeMax=Sunday 23:59:59
Next: Use eventId AND calendarId with 'update_event' or 'delete_event'.
Example in this repo: Tool descriptions focus on semantics/examples and next
steps, while argument docs live in inputSchema and structured output docs live
in outputSchema.
3. Tool Annotations
What Great Servers Do
Every tool includes annotations that help the AI understand the tool's behavior:
annotations: {
readOnlyHint: true, // Does not modify state
destructiveHint: false, // Does not delete data
idempotentHint: true, // Safe to call multiple times
openWorldHint: true, // May access external resources
}
Guidelines:
| Annotation | When to use true |
|---|---|
readOnlyHint | GET/LIST operations |
destructiveHint | DELETE operations, irreversible changes |
idempotentHint | Same input always produces same result |
openWorldHint | Accesses external APIs/resources |
Example in this repo: All tools provide annotations via the
server.registerTool() config.
4. Schema Patterns (Input & Output)
What Great Servers Do
Rich, descriptive schemas with:
- Clear descriptions for each field
- Default values explained (where defaults exist)
- Valid values listed (especially for enums)
- Format expectations (dates, IDs, etc.)
Use:
inputSchemato document tool arguments (types, defaults, constraints).outputSchemato document the shape ofstructuredContenton success. If anoutputSchemais provided, the SDK validates thatstructuredContentexists and matches the schema for non-error tool results.
Example (input schema):
z.object({
calendarId: z
.union([z.literal('all'), z.string(), z.array(z.string())])
.optional()
.default('all')
.describe(
'Calendar ID(s). Use "all" (default) to search all calendars, a single ID, or array of IDs',
),
timeMin: z
.string()
.optional()
.describe(
'Start of time range (RFC3339 with timezone, e.g., 2025-12-06T19:00:00Z)',
),
maxResults: z
.number()
.int()
.min(1)
.max(250)
.optional()
.default(50)
.describe('Max events to return (1-250, default: 50)'),
})
Example (output schema):
outputSchema: {
app_id: z.string().describe('Persisted app identifier'),
title: z.string().describe('Human-facing app title'),
runtime: z.enum(['html', 'javascript']),
}
Example in this repo: Tool schemas describe defaults, valid values, and
format expectations (where applicable), and tools provide outputSchema for the
shape of structuredContent on success.
5. Response Formatting
What Great Servers Do
Return both human-readable text AND structured content:
return {
content: [
{
type: 'text',
text: `✓ Event created: [${title}](${htmlLink})\n when: ${start}\n meet: ${meetLink}`,
},
],
structuredContent: {
id: event.id,
summary: event.summary,
// ... full structured data
},
}
Human-readable text patterns:
- Use markdown formatting (links, bold, lists)
- Use emojis for status (✓, ⚠️, 🟢, 🔴)
- Include context (what calendar, which feed)
- Provide next steps in the text
Example from Tesla:
## Model 3
**Status**: asleep
**Locked**: Yes ✓
**Sentry Mode**: On
### Battery
- Level: 78%
- Range: 312 km
- Charging: Not charging
### ⚠️ Open
- Trunk
Example in this repo: Tools return human-readable markdown in content and
machine-friendly data in structuredContent. Tool descriptions should not
mention these protocol field names.
6. Tool Modules (One Tool Per File)
What Great Servers Do
Prefer one tool per file, with the tool's description, annotations, schemas,
and handler colocated. Keep a small register-tools module that imports each
tool module and registers them.
// packages/worker/src/mcp/tools/search.ts
export async function registerSearchTool(agent: MCP) {
agent.server.registerTool(
'search',
{/* metadata + schemas */},
async (args) => {
// handler
},
)
}
// packages/worker/src/mcp/register-tools.ts
import { registerSearchTool } from './tools/search.ts'
export async function registerTools(agent: MCP) {
await registerSearchTool(agent)
}
Benefits:
- Smaller diffs and less merge conflict
- Tool docs/schemas/handler stay in sync
- Easier to add/remove tools without touching unrelated tools
Example in this repo: Server instructions are built by
buildMcpServerInstructions in packages/worker/src/mcp/server-instructions.ts
(with fragments under packages/worker/src/mcp/instructions/). The snippet
above is the generic pattern for small tools; the search tool deliberately
splits its responsibilities across metadata/schemas
(packages/worker/src/mcp/tools/search-tool-definition.ts), registration
(search-register.ts), and the handler (search-tool-runner.ts) because each
piece grew large, with packages/worker/src/mcp/register-tools.ts as the small
aggregator. Preserve that split rather than folding it back into one file.
7. Tool Naming Conventions
What Great Servers Do
| Pattern | Example | Use Case |
|---|---|---|
list_* | list_feeds, list_users | Get multiple items |
get_* | get_feed, get_issue | Get single item by ID |
create_* | create_feed | Create new item |
update_* | update_feed | Modify existing item |
delete_* | delete_feed | Remove item |
browse_* | browse_media | Navigate/explore |
search_* | search_events | Query with filters |
Consistency rules:
- Use
snake_casefor tool names - Group related tools with common prefix
- Use singular nouns for get/create, plural for list
Example in this repo: Tool names use snake_case.
8. Error Handling
What Great Servers Do
Provide helpful, actionable error messages:
if (!feed) {
return {
content: [
{
type: 'text',
text: `Feed "${feedId}" not found.\n\nNext: Use list_feeds to see available feeds.`,
},
],
isError: true,
}
}
Patterns:
- Explain what went wrong
- Suggest how to fix it
- Reference related tools that can help
- Include valid values when applicable
Example in this repo: Tool error responses include actionable next steps (including which tool to call next).
9. Pagination & Limiting
What Great Servers Do
Consistent pagination patterns:
return {
content: [...],
structuredContent: {
items: [...],
pagination: {
hasMore: boolean,
nextCursor: string | undefined,
itemsReturned: number,
limit: number,
},
},
}
In schemas + descriptions:
Put the output shape in `outputSchema` (for `structuredContent`), and describe
the chaining semantics in the tool description:
Next:
- Pass `pagination.nextCursor` to fetch the next page.
Example in this repo: The example tool does not paginate, but this pattern fits list-style tools that need pagination.
10. Resource Patterns
What Great Servers Do
Resources provide read-only data access with:
- Clear URI schemes (
media://feeds,media://feeds/{id}) - Proper MIME types
- Descriptions that explain the data structure
Good resource examples:
media://server— Server info and statisticsmedia://feeds— All feeds listmedia://feeds/{id}— Individual feed detailsmedia://directories— Available media directories
Example in this repo: Resources are not registered, but this pattern is recommended for exposing read-only docs and server metadata.
11. Prompt Patterns
What Great Servers Do
Prompts are task-oriented conversation starters:
- Guide the user through multi-step workflows
- Provide context about available tools
- Include concrete next steps
- Support optional parameters to customize the task
Example prompt:
I want to create a new feed. Please help me decide:
1. Should this be a directory feed (automatically includes all media from a folder)?
2. Or a curated feed (manually select specific content)?
Available media roots:
- audio: /media/audio
- video: /media/video
Please ask me some questions to understand what I'm trying to create, then help me set it up.
Example in this repo: Prompts are not registered, but this pattern is recommended for guiding multi-step workflows.