ADR-010: MCP Resource Pattern Redesign
Status: Accepted Date: 2025-10-09 Deciders: Development Team Context: MCP Best Practices Review
Context and Problem Statement
During an MCP best practices review (2025-10-09), a critical architectural misalignment was identified: DocuMCP was using MCP resources as a persistence layer to store tool execution results, violating the fundamental MCP control pattern philosophy.
The Problem:
- Resources were storing tool outputs via
storeResourceFromToolResult() - A
resourceStoreMap held dynamic tool results - Resource URIs were generated at runtime (e.g.,
documcp://analysis/{timestamp}-{random}) - This violated MCP's core principle that resources should serve applications, not store tool results
Why This Matters: According to MCP best practices, the three primitives have distinct control patterns:
- Tools = Model-controlled (Claude decides when to execute) → Serve the model
- Resources = App-controlled (application decides when to fetch) → Serve the app
- Prompts = User-controlled (user triggers via actions) → Serve users
Using resources for tool result storage conflates model operations with app operations, creating architectural confusion and misusing the MCP protocol.
Decision Drivers
Technical Requirements
- Align with MCP specification and best practices
- Follow proper control pattern separation
- Maintain backward compatibility where possible
- Preserve existing tool functionality
Architectural Principles
- Separation of Concerns: Tools handle execution, resources provide app data
- Statelessness: MCP servers should be stateless; persistence belongs elsewhere
- Clear Purpose: Each primitive serves its intended audience
Developer Experience
- Simplify resource implementation
- Make resource purpose obvious
- Enable proper MCP Inspector testing
Considered Options
Option 1: Keep Current Pattern (Status Quo) ❌
Description: Continue using resources to store tool results.
Pros:
- No code changes required
- Existing URIs remain functional
- No migration needed
Cons:
- ❌ Violates MCP best practices
- ❌ Confuses model operations with app operations
- ❌ Makes MCP Inspector testing unclear
- ❌ Creates unnecessary complexity
- ❌ Misrepresents resource purpose
Decision: Rejected due to architectural misalignment
Option 2: Remove All Resources ❌
Description: Eliminate resources entirely, return all data via tools only.
Pros:
- Simplifies implementation
- Eliminates resource confusion
- Focuses on tools as primary interface
Cons:
- ❌ Removes legitimate use cases for app-controlled data
- ❌ Loses template access for UI
- ❌ Prevents SSG list for dropdowns
- ❌ Underutilizes MCP capabilities
Decision: Rejected - throws baby out with bathwater
Option 3: Redesign Resources for App Needs ✅ (CHOSEN)
Description: Remove tool result storage, create static resources that serve application UI needs.
Pros:
- ✅ Aligns with MCP best practices
- ✅ Clear separation: tools execute, resources provide app data
- ✅ Enables proper MCP Inspector testing
- ✅ Provides legitimate value to applications
- ✅ Follows control pattern philosophy
Cons:
- Requires code refactoring
- Changes resource URIs (but tools remain compatible)
Decision: ACCEPTED - Best aligns with MCP architecture
Decision Outcome
Chosen Option: Option 3 - Redesign Resources for App Needs
Implementation Details
1. Remove Tool Result Storage
Before:
const resourceStore = new Map<string, { content: string; mimeType: string }>();
function storeResourceFromToolResult(
toolName: string,
args: any,
result: any,
id?: string,
): string {
const uri = `documcp://analysis/${id}`;
resourceStore.set(uri, {
content: JSON.stringify(result),
mimeType: "application/json",
});
return uri;
}
// In tool handler:
const result = await analyzeRepository(args);
const resourceUri = storeResourceFromToolResult(
"analyze_repository",
args,
result,
);
(result as any).resourceUri = resourceUri;
return result;
After:
// No resource storage! Tools return results directly
const result = await analyzeRepository(args);
return wrapToolResult(result, "analyze_repository");
2. Create Static App-Serving Resources
New Resource Categories:
A. SSG List Resource (for UI dropdowns)
{
uri: "documcp://ssgs/available",
name: "Available Static Site Generators",
description: "List of supported SSGs with capabilities for UI selection",
mimeType: "application/json"
}
Returns:
{
"ssgs": [
{
"id": "jekyll",
"name": "Jekyll",
"description": "Ruby-based SSG, great for GitHub Pages",
"language": "ruby",
"complexity": "low",
"buildSpeed": "medium",
"ecosystem": "mature",
"bestFor": ["blogs", "documentation", "simple-sites"]
}
// ... 4 more SSGs
]
}
B. Configuration Templates (for SSG setup)
{
uri: "documcp://templates/jekyll-config",
name: "Jekyll Configuration Template",
description: "Template for Jekyll _config.yml",
mimeType: "text/yaml"
}
Returns actual YAML template for Jekyll configuration.
C. Workflow Resources (for UI workflow display)
{
uri: "documcp://workflows/all",
name: "All Documentation Workflows",
description: "Complete list of available documentation workflows",
mimeType: "application/json"
}
3. Resource Handler Implementation
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// Handle SSG list (for UI)
if (uri === "documcp://ssgs/available") {
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify({ ssgs: [...] })
}]
};
}
// Handle templates (static content)
if (uri.startsWith("documcp://templates/")) {
const templateType = uri.split("/").pop();
return {
contents: [{
uri,
mimeType: getTemplateMimeType(templateType),
text: getTemplateContent(templateType)
}]
};
}
throw new Error(`Resource not found: ${uri}`);
});
Resource Design Principles
- Static Content Only: Resources return pre-defined, static data
- App-Controlled: Applications fetch resources when needed for UI
- Predictable URIs: Fixed URIs (no timestamps or random IDs)
- Clear Purpose: Each resource serves a specific app UI need
Consequences
Positive Consequences ✅
-
Architectural Alignment
- Resources now properly serve applications
- Clear separation between tools and resources
- Follows MCP control pattern philosophy
-
Improved Developer Experience
- Resource purpose is obvious
- MCP Inspector testing is clear
- No confusion about resource lifecycle
-
Better Testability
- Resources return predictable content
- Can test resources independently
- MCP Inspector works correctly
-
Simplified Implementation
- Removed
resourceStoreMap - Removed
storeResourceFromToolResult()function - Removed 50+ lines of resource storage code
- Tools are simpler (no resource URI tracking)
- Removed
-
Legitimate App Value
- SSG list enables UI dropdowns
- Templates provide boilerplate content
- Workflows guide user actions
Negative Consequences ⚠️
-
Breaking Change for Resource URIs
- Old dynamic URIs (
documcp://analysis/{timestamp}) no longer work - Applications relying on these URIs need updates
- Mitigation: Tools return data directly; URIs were internal implementation detail
- Old dynamic URIs (
-
No Tool Result Persistence
- Tool results are not stored between executions
- Applications must handle result storage if needed
- Mitigation: MCP servers should be stateless; persistence is app responsibility
-
Migration Effort
- Required updating all tool handlers
- Updated resource definitions
- Time Cost: ~4 hours
Implementation Results
Code Changes
Files Modified:
src/index.ts(main server file)- Removed
resourceStoreMap (10 lines) - Removed
storeResourceFromToolResult()(50 lines) - Redesigned
RESOURCESarray (12 new resources) - Updated
ReadResourceRequestSchemahandler (150 lines) - Removed resource storage from all tools (30+ locations)
- Removed
Lines of Code:
- Removed: ~120 lines (resource storage logic)
- Added: ~200 lines (static resource handlers)
- Net Change: +80 lines (but much clearer purpose)
Test Results
Before Implementation:
- Tests: 122/122 passing ✅
- TypeScript: Compiles ✅
After Implementation:
- Tests: 122/122 passing ✅
- TypeScript: Compiles ✅
- No broken tests
- No regression issues
Performance Impact
Before:
- Resource storage: O(1) Map insertion per tool
- Memory: Growing Map of all tool results
After:
- Resource retrieval: O(1) static content lookup
- Memory: Fixed size (no growth)
Improvement: Reduced memory usage, no performance degradation
Compliance with MCP Best Practices
Before Redesign
- Resource Implementation: 3/10 ❌
- Control Patterns: 4/10 ❌
After Redesign
- Resource Implementation: 9/10 ✅
- Control Patterns: 9/10 ✅
Migration Guide
For Client Applications
Old Pattern (No Longer Works):
// Execute tool
const result = await callTool("analyze_repository", { path: "./" });
// WRONG: Try to fetch from resource URI
const resourceUri = result.resourceUri;
const resource = await readResource(resourceUri); // ❌ Will fail
New Pattern (Recommended):
// Execute tool - result contains all data
const result = await callTool("analyze_repository", { path: "./" });
// Use result directly (no need for resources)
console.log(result.data); // ✅ All data is here
// Use resources for app UI needs
const ssgList = await readResource("documcp://ssgs/available"); // ✅ For dropdowns
const template = await readResource("documcp://templates/jekyll-config"); // ✅ For setup
For Tool Developers
Old Pattern:
const result = await analyzeRepository(args);
const resourceUri = storeResourceFromToolResult(
"analyze_repository",
args,
result,
);
(result as any).resourceUri = resourceUri;
return result;
New Pattern:
const result = await analyzeRepository(args);
return wrapToolResult(result, "analyze_repository"); // Standardized wrapper
References
- MCP Specification: https://modelcontextprotocol.io/docs
- MCP Best Practices Review:
MCP_BEST_PRACTICES_REVIEW.md - MCP Inspector Guide:
docs/development/MCP_INSPECTOR_TESTING.md - Related ADRs:
- ADR-006: MCP Tools API Design
- ADR-007: MCP Prompts and Resources Integration
Notes
Design Philosophy
The resource redesign embodies a core MCP principle: each primitive serves its audience.
- Tools answer the question: "What can Claude do?"
- Resources answer the question: "What data does my app need?"
- Prompts answer the question: "What workflows can users trigger?"
Mixing these purposes creates architectural debt and violates separation of concerns.
Future Enhancements
Potential Additional Resources:
documcp://themes/available- UI theme listdocumcp://validators/rules- Validation rule catalogdocumcp://examples/{category}- Example content library
These should all follow the same principle: serve the application's UI needs, not store execution results.
Last Updated: 2025-10-09 Status: Implemented and Verified ✅