featurebase-mcp
Version:
MCP server for Featurebase API - manage posts and comments
263 lines (182 loc) • 7.49 kB
Markdown
# Taming Chatty APIs: Implementing Smart Field Selection in MCP Servers
Model Context Protocol (MCP) servers that proxy to REST APIs face a common challenge: API responses can be incredibly verbose, leading to token exhaustion, context window bloat, and poor signal-to-noise ratios. When an AI agent just needs a post title and author name, why should it receive 50 fields of metadata, nested objects, and arrays?
## The Problem: API Verbosity Kills Performance
REST APIs are designed for flexibility, often returning comprehensive data structures. Consider a typical blog post response from an API:
```json
{
"id": "123",
"title": "My Blog Post",
"content": "Lorem ipsum...",
"author": {
"id": "456",
"name": "John Doe",
"email": "john@example.com",
"profilePicture": "https://...",
"bio": "A long biography...",
"socialLinks": {...},
"preferences": {...}
},
"organization": {
"id": "789",
"name": "Acme Corp",
"domain": "acme.com",
"settings": {...},
"billing": {...}
},
"metadata": {
"views": 1234,
"likes": 56,
"comments": 78,
"lastModified": "2024-01-01T00:00:00Z",
"seoData": {...},
"analytics": {...}
},
// ... dozens more fields
}
```
When an AI agent asks "What are the recent blog posts?", it doesn't need billing information, SEO data, or full user profiles. This verbose response:
1. **Wastes tokens** - Each unnecessary field costs money
2. **Fills context windows** - Less room for actual conversation
3. **Reduces accuracy** - Important information gets lost in noise
4. **Slows processing** - More data to parse and understand
## The Solution: Auto-Discovery Field Selection
The ideal solution lets agents dynamically specify which fields they want, similar to GraphQL's field selection but simpler. Here's what we implemented:
### 1. Rich Tool Descriptions with Field Discovery
Instead of generic descriptions, we embedded field schemas directly in tool descriptions:
```typescript
{
name: 'list_posts',
description: 'List posts with optional filtering. Available fields: id, title, content, author(name,email), organization(name), upvotes, postCategory(category), postTags(name), postStatus(name), date, lastModified'
}
```
This gives agents immediate visibility into:
- Available top-level fields (`id`, `title`, `content`)
- Nested object fields (`author(name,email)`)
- Array structures (`postTags(name)`)
### 2. JSON Mask Selection Syntax
We chose the proven `json-mask` library syntax that's intuitive for AI agents:
```javascript
// Simple field selection
"id,title,upvotes"
// Nested object selection
"title,author(name,email)"
// Array element selection
"postTags(name),author(name)"
// Complex combinations
"id,title,author(name,email),postCategory(category),postTags(name)"
```
### 3. Optional Parameter Design
The `select` parameter is completely optional:
```typescript
select: {
type: 'string',
description: 'Fields to return. Examples: "id,title,upvotes" | "title,author(name)" | "postCategory(category),postStatus(name)". Leave empty for all fields.',
}
```
No selection = full unfiltered response. This ensures backward compatibility and lets agents choose their level of optimization.
## Implementation Details
### Using the JSON Mask Library
Rather than reinventing the wheel, we leveraged the battle-tested `json-mask` library:
```bash
npm install json-mask @types/json-mask
```
```typescript
import mask from 'json-mask';
```
The beauty of `json-mask` is its simplicity - it handles all the complex parsing and field selection logic for us.
### Integration with Request Handlers
The integration is remarkably simple:
```typescript
case 'list_posts': {
const { select, ...apiArgs } = args as any;
const result = await this.api.listPosts(apiArgs);
const filtered = select
? { ...result, results: result.results?.map((post: any) => mask(post, select)) || [] }
: result;
return {
content: [{
type: 'text',
text: JSON.stringify(filtered, null, 2),
}],
};
}
```
Just `mask(data, selectString)` - that's it! No complex parsing, no recursive functions, no edge case handling.
## Real-World Impact
### Before Field Selection
```bash
Agent: "Show me recent posts"
Response: 15KB of JSON with full user profiles, organization details, metadata...
Tokens: ~4,000
Context: Heavily polluted
```
### After Field Selection
```bash
Agent: "Show me recent posts" (with select: "id,title,author(name)")
Response: 2KB of clean, focused data
Tokens: ~500
Context: Clean and relevant
```
**Result: 87% reduction in response size and token usage.**
## Why This Approach Works
### 1. **Autodiscovery Through Documentation**
Agents learn available fields from tool descriptions without needing separate schema introspection calls.
### 2. **Proven Syntax**
The `json-mask` syntax is battle-tested and feels natural to AI agents that understand nested data structures.
### 3. **Progressive Enhancement**
Works perfectly without field selection, adds optimization when needed.
### 4. **Zero Breaking Changes**
Existing integrations continue working unchanged.
### 5. **Simple Implementation**
Using `json-mask` makes it trivial to add to any MCP server - just one function call.
## Best Practices for Implementation
### 1. Keep Field Descriptions Current
Update tool descriptions when API schemas change:
```typescript
// ❌ Outdated
description: 'Get users. Fields: id, name, email'
// ✅ Current and comprehensive
description: 'Get users. Available fields: id, name, email, profile(avatar,bio), preferences(theme,notifications), company(name,role)'
```
### 2. Provide Realistic Examples
Show common use cases in parameter descriptions:
```typescript
select: {
description: 'Fields to return. Examples: "name,email" | "profile(avatar)" | "company(name,role)". Leave empty for all fields.'
}
```
### 3. Let JSON Mask Handle the Details
```typescript
// json-mask handles missing fields, nested structures, and arrays automatically
const filtered = mask(data, selectString);
```
### 4. Consider Performance
For large datasets, field selection can dramatically improve performance:
```typescript
// Before: 1000 posts × 50 fields = 50,000 data points
// After: 1000 posts × 3 fields = 3,000 data points
```
## Future Enhancements
### 1. **Field Aliasing**
```javascript
"author(name:authorName,email:authorEmail)"
```
### 2. **Conditional Fields**
```javascript
"author(name,email?admin)" // email only if admin
```
### 3. **Advanced Filtering**
```javascript
"posts(title,author(name),createdAt|date>2024-01-01)"
```
### 4. **Field Validation**
Validate requested fields against schema and provide helpful error messages.
## Conclusion
Field selection transforms chatty APIs into efficient, focused data sources. By leveraging the proven `json-mask` library with auto-discovery through rich documentation, we've created a system that:
- **Reduces token costs by 80-90%**
- **Improves response relevance**
- **Maintains backward compatibility**
- **Takes just minutes to implement**
The next time you're building an MCP server that proxies to a verbose API, just `npm install json-mask` and add a `select` parameter. Your AI agents (and your token bill) will thank you.
*Want to see this in action? Check out the [Featurebase MCP Server](https://github.com/marcinwyszynski/featurebase-mcp) implementation that demonstrates these patterns.*