📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
# MCP Server Patterns
|
||||
|
||||
Deep dive into @outfitter/mcp patterns.
|
||||
|
||||
## Creating a Server
|
||||
|
||||
```typescript
|
||||
import { createMcpServer, defineTool } from "@outfitter/mcp";
|
||||
|
||||
const server = createMcpServer({
|
||||
name: "my-server",
|
||||
version: "0.1.0",
|
||||
description: "Server for AI agents",
|
||||
});
|
||||
|
||||
// Register tools before start
|
||||
server.registerTool(searchTool);
|
||||
server.registerTool(createTool);
|
||||
|
||||
// Start server
|
||||
server.start();
|
||||
```
|
||||
|
||||
## Tool Definition
|
||||
|
||||
### Using defineTool()
|
||||
|
||||
The `defineTool()` helper provides full type inference from the Zod schema:
|
||||
|
||||
```typescript
|
||||
import { defineTool } from "@outfitter/mcp";
|
||||
import { Result, ValidationError } from "@outfitter/contracts";
|
||||
import { z } from "zod";
|
||||
|
||||
const InputSchema = z.object({
|
||||
query: z.string().min(1).describe("Search query"),
|
||||
limit: z.number().int().positive().default(10).describe("Max results"),
|
||||
});
|
||||
|
||||
export const searchTool = defineTool({
|
||||
name: "search",
|
||||
description: "Search for items. Use when user asks to find or search.",
|
||||
inputSchema: InputSchema,
|
||||
|
||||
handler: async (input): Promise<Result<SearchOutput, ValidationError>> => {
|
||||
// input is automatically typed from InputSchema
|
||||
const results = await performSearch(input.query, input.limit);
|
||||
return Result.ok({ results, total: results.length });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Schema Best Practices
|
||||
|
||||
```typescript
|
||||
const InputSchema = z.object({
|
||||
// Always use .describe() for AI understanding
|
||||
query: z.string().describe("The search term to look for"),
|
||||
|
||||
// Provide defaults where sensible
|
||||
limit: z.number().default(10).describe("Maximum number of results"),
|
||||
|
||||
// Use enums for fixed choices
|
||||
sortBy: z.enum(["name", "date", "relevance"]).default("relevance")
|
||||
.describe("Field to sort results by"),
|
||||
|
||||
// Mark optional fields explicitly
|
||||
tags: z.array(z.string()).optional().describe("Filter by tags"),
|
||||
});
|
||||
```
|
||||
|
||||
### Tool with Context
|
||||
|
||||
```typescript
|
||||
export const myTool = defineTool({
|
||||
name: "my_tool",
|
||||
description: "Tool description",
|
||||
inputSchema: InputSchema,
|
||||
|
||||
handler: async (input, ctx) => {
|
||||
ctx.logger.debug("Tool invoked", { input });
|
||||
|
||||
const result = await myHandler(input, ctx);
|
||||
|
||||
if (result.isErr()) {
|
||||
ctx.logger.error("Tool failed", { error: result.error });
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### Static Resource
|
||||
|
||||
```typescript
|
||||
server.registerResource({
|
||||
uri: "config://settings",
|
||||
name: "Configuration",
|
||||
description: "Current server configuration",
|
||||
mimeType: "application/json",
|
||||
|
||||
read: async () => {
|
||||
return JSON.stringify(config, null, 2);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Dynamic Resource
|
||||
|
||||
```typescript
|
||||
server.registerResource({
|
||||
uri: "data://users/{id}",
|
||||
name: "User Data",
|
||||
description: "User information by ID",
|
||||
mimeType: "application/json",
|
||||
|
||||
read: async (uri) => {
|
||||
const id = uri.split("/").pop();
|
||||
const user = await getUser(id);
|
||||
return JSON.stringify(user);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Resource List
|
||||
|
||||
```typescript
|
||||
server.registerResourceList({
|
||||
uri: "data://users",
|
||||
name: "Users",
|
||||
description: "List of all users",
|
||||
|
||||
list: async () => {
|
||||
const users = await getAllUsers();
|
||||
return users.map(u => ({
|
||||
uri: `data://users/${u.id}`,
|
||||
name: u.name,
|
||||
description: u.email,
|
||||
}));
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Prompts
|
||||
|
||||
```typescript
|
||||
server.registerPrompt({
|
||||
name: "analyze",
|
||||
description: "Analyze data with specific focus",
|
||||
arguments: [
|
||||
{ name: "focus", description: "What to focus on", required: true },
|
||||
{ name: "depth", description: "Analysis depth", required: false },
|
||||
],
|
||||
|
||||
get: async (args) => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `Analyze with focus on: ${args.focus}. Depth: ${args.depth || "normal"}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Returning Errors
|
||||
|
||||
```typescript
|
||||
handler: async (input) => {
|
||||
if (!input.query) {
|
||||
return Result.err(new ValidationError("Query is required"));
|
||||
}
|
||||
|
||||
const item = await findItem(input.id);
|
||||
if (!item) {
|
||||
return Result.err(new NotFoundError("item", input.id));
|
||||
}
|
||||
|
||||
return Result.ok(item);
|
||||
}
|
||||
```
|
||||
|
||||
### Error Categories in MCP
|
||||
|
||||
| Category | MCP Behavior |
|
||||
|----------|--------------|
|
||||
| validation | Tool returns error with details |
|
||||
| not_found | Tool returns error with resource info |
|
||||
| internal | Tool returns generic error, logs full error |
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "bun",
|
||||
"args": ["run", "/path/to/server.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Environment Variables
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "bun",
|
||||
"args": ["run", "/path/to/server.ts"],
|
||||
"env": {
|
||||
"API_KEY": "secret",
|
||||
"LOG_LEVEL": "debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deferred Tool Loading
|
||||
|
||||
For tools that are expensive to load:
|
||||
|
||||
```typescript
|
||||
import { defineDeferredTool } from "@outfitter/mcp";
|
||||
|
||||
const heavyTool = defineDeferredTool({
|
||||
name: "heavy_tool",
|
||||
description: "Expensive tool loaded on demand",
|
||||
|
||||
load: async () => {
|
||||
const { heavyTool } = await import("./heavy-tool.js");
|
||||
return heavyTool;
|
||||
},
|
||||
});
|
||||
|
||||
// Deferred tools use the same registerTool() API - the server
|
||||
// detects the deferred wrapper and handles lazy loading internally
|
||||
server.registerTool(heavyTool);
|
||||
```
|
||||
|
||||
## Testing MCP Servers
|
||||
|
||||
```typescript
|
||||
import { createMcpHarness } from "@outfitter/testing";
|
||||
|
||||
const harness = createMcpHarness(myTool);
|
||||
|
||||
test("tool returns results", async () => {
|
||||
const result = await harness.invoke({ query: "test" });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value.results).toHaveLength(3);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Descriptive schemas** - Use `.describe()` on every field
|
||||
2. **Sensible defaults** - Provide `.default()` where appropriate
|
||||
3. **Error categories** - Use taxonomy errors for proper handling
|
||||
4. **Logging** - Log tool invocations for debugging
|
||||
5. **Deferred loading** - Lazy load expensive tools
|
||||
6. **Test harnesses** - Use `createMcpHarness` for testing
|
||||
Reference in New Issue
Block a user