Skip to main content

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 resourceStore Map 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

  1. Static Content Only: Resources return pre-defined, static data
  2. App-Controlled: Applications fetch resources when needed for UI
  3. Predictable URIs: Fixed URIs (no timestamps or random IDs)
  4. Clear Purpose: Each resource serves a specific app UI need

Consequences

Positive Consequences ✅

  1. Architectural Alignment

    • Resources now properly serve applications
    • Clear separation between tools and resources
    • Follows MCP control pattern philosophy
  2. Improved Developer Experience

    • Resource purpose is obvious
    • MCP Inspector testing is clear
    • No confusion about resource lifecycle
  3. Better Testability

    • Resources return predictable content
    • Can test resources independently
    • MCP Inspector works correctly
  4. Simplified Implementation

    • Removed resourceStore Map
    • Removed storeResourceFromToolResult() function
    • Removed 50+ lines of resource storage code
    • Tools are simpler (no resource URI tracking)
  5. Legitimate App Value

    • SSG list enables UI dropdowns
    • Templates provide boilerplate content
    • Workflows guide user actions

Negative Consequences ⚠️

  1. 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
  2. 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
  3. 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 resourceStore Map (10 lines)
    • Removed storeResourceFromToolResult() (50 lines)
    • Redesigned RESOURCES array (12 new resources)
    • Updated ReadResourceRequestSchema handler (150 lines)
    • Removed resource storage from all tools (30+ locations)

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 list
  • documcp://validators/rules - Validation rule catalog
  • documcp://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 ✅