Skip to content
Automation
Skill

/mcp-tool-resource-pattern

Implements the core MCP Apps architectural pattern where a Tool declares _meta.ui.resourceUri referencing a registered Resource. Covers registerAppTool, registerAppResource, text fallback, structuredContent, and app-only helper tools.

From plugin
babysitter
1.8k200 skills3 agents21 commands1 MCP
Install
$ npx -y skills add a5c-ai/babysitter --skill mcp-tool-resource-pattern --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/mcp-tool-resource-pattern

Context preview

The summary Claude sees to decide when to auto-load this skill.

Implements the core MCP Apps architectural pattern where a Tool declares _meta.ui.resourceUri referencing a registered Resource. Covers registerAppTool, registerAppResource, text fallback, structuredContent, and app-only helper tools.

SKILL.md

mcp-tool-resource-pattern.SKILL.md
name: mcp-tool-resource-pattern
description: Implements the core MCP Apps architectural pattern where a Tool declares _meta.ui.resourceUri referencing a registered Resource. Covers registerAppTool, registerAppResource, text fallback, structuredContent, and app-only helper tools.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
graph:
  domains: [domain:software-engineering]
  specializations: [specialization:ai-agents-conversational]
  skillAreas: [skill-area:mcp-tool-design, skill-area:mcp-resource-design]
  roles: [role:backend-engineer, role:fullstack-engineer]
  workflows: [workflow:feature-development]
  topics: [topic:api-design, topic:design-patterns]

mcp-tool-resource-pattern

Implement the foundational Tool + Resource pattern that every MCP App requires: a Tool that returns data and references a Resource that serves the interactive UI.

Overview

Every MCP App is built on the Tool + Resource pattern:

1. **Tool** (registered via `registerAppTool`): Called by the LLM/host, returns data. Its `_meta.ui.resourceUri` tells the host which Resource provides the UI. 2. **Resource** (registered via `registerAppResource`): Serves a bundled HTML file that renders the interactive UI in a sandboxed iframe. 3. The tool passes data to the UI via `structuredContent` (available in `ontoolresult` handler). 4. The tool MUST also return a `content` array with text fallback for non-UI hosts.

Capabilities

registerAppTool Implementation

  • Register tools with `_meta.ui.resourceUri` linking to a resource
  • Pass data via `structuredContent` for rich UI rendering
  • Always include `content` array with text fallback
  • Configure tool input schemas via Zod

registerAppResource Implementation

  • Register HTML resources with `RESOURCE_MIME_TYPE`
  • Serve single-file bundled HTML
  • Configure CSP domains in `contents[]` return
  • Support multiple tools sharing the same resource URI

App-Only Helper Tools

  • Create tools with `visibility: ['app']` -- only callable from the UI iframe, not by the LLM
  • Use cases: polling for updates, loading additional data, pagination, state mutations
  • Implement via `app.callServerTool()` from client-side

Graceful Degradation

  • Detect UI capability via `getUiCapability()` on the server
  • Return richer responses when UI is available
  • Always maintain text-only fallback path

Usage

Basic Tool + Resource Pattern

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
  registerAppTool,
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps';
import { z } from 'zod';
import fs from 'fs';
import path from 'path';

const server = new McpServer({ name: 'my-app', version: '1.0.0' });

// Read the bundled HTML (built by vite-plugin-singlefile)
const bundledHtml = fs.readFileSync(
  path.join(__dirname, '../dist/mcp-app.html'),
  'utf-8'
);

// 1. Register the Resource (serves the UI)
registerAppResource(server, {
  uri: 'app:///my-app',
  name: 'My App UI',
  mimeType: RESOURCE_MIME_TYPE,
  async read() {
    return {
      contents: [{
        uri: 'app:///my-app',
        mimeType: RESOURCE_MIME_TYPE,
        text: bundledHtml,
        // CSP domains (if needed)
        // resourceDomains: ['https://cdn.example.com'],
        // connectDomains: ['https://api.example.com'],
      }],
    };
  },
});

// 2. Register the Tool (returns data, references the resource)
registerAppTool(server, {
  name: 'show_dashboard',
  description: 'Show an interactive dashboard',
  inputSchema: {
    type: 'object' as const,
    properties: {
      query: { type: 'string', description: 'Search query' },
    },
    required: ['query'],
  },
  // _meta.ui.resourceUri is set automatically by registerAppTool
  resourceUri: 'app:///my-app',
  async handler(args) {
    const data = await fetchDashboardData(args.query);

    return {
      // Text fallback for non-UI hosts (REQUIRED)
      content: [
        {
          type: 'text' as const,
          text: `Dashboard results for "${args.query}":\n${formatAsText(data)}`,
        },
      ],
      // Rich data for the UI (available in ontoolresult handler)
      structuredContent: {
        query: args.query,
        results: data.results,
        metadata: data.metadata,
      },
    };
  },
});

App-Only Helper Tools

// This tool is ONLY callable from the UI iframe via app.callServerTool()
// The LLM/host cannot call it directly
registerAppTool(server, {
  name: 'load_page',
  description: 'Load a specific page of results',
  visibility: ['app'],  // App-only: not visible to LLM
  inputSchema: {
    type: 'object' as const,
    properties: {
      page: { type: 'number' },
      pageSize: { type: 'number' },
    },
    required: ['page'],
  },
  resourceUri: 'app:///my-app',
  async handler(args) {
    const data = await fetchPage(args.page, args.pageSize || 20);
    return {
      content: [{ type: 'text' as const, text: JSON.stringify(data) }],
      structuredContent: data,
    };
  },
});

Client-Side: Calling App-Only Tools

import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';

const app = new App({ transport: new PostMessageTransport() });

// Call an app-only tool from the UI
async function loadNextPage(page: number) {
  const result = await app.callServerTool('load_page', {
    page,
    pageSize: 20,
  });
  renderResults(result.structuredContent);
}

Multiple Tools Sharing One Resource

// Both tools reference the same resource URI
// The UI handles both by checking which tool triggered

registerAppTool(server, {
  name: 'search_products',
  description: 'Search for products',
  resourceUri: 'app:///product-viewer',
  // ...
});

registerAppTool(server, {
  name: 'show_product_details',
  description: 'Show details for a specific product',
  resourceUri: 'app:///product-viewer',  // Same resource
  // ...
});

// In the UI, distinguish via ontoolinput handl
Read more
Ships withbabysitter

Enforce obedience on agentic workforces. Manage extremely complex workflows through deterministic, hallucination-free self-orchestration.

Get the whole plugin

Other skills on babysitter.