본문으로 건너뛰기
cccskills
무료GitHub에 공개

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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(2)

  • SKILL.md10.3 KB
  • README.md2.7 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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 handler:
app.ontoolinput = (params) => {
  if (params.toolName === 'search_products') {
    renderSearchResults(params.arguments);
  } else if (params.toolName === 'show_product_details') {
    renderProductDetails(params.arguments);
  }
};

Graceful Degradation

import { getUiCapability } from '@modelcontextprotocol/ext-apps';

registerAppTool(server, {
  name: 'show_chart',
  description: 'Display data as a chart',
  resourceUri: 'app:///chart-viewer',
  async handler(args) {
    const data = await getData(args);
    const uiCapability = getUiCapability();

    // Rich response when UI is available
    if (uiCapability === 'full') {
      return {
        content: [{ type: 'text' as const, text: formatAsTable(data) }],
        structuredContent: {
          chartType: 'bar',
          labels: data.labels,
          values: data.values,
        },
      };
    }

    // Text-only response for non-UI hosts
    return {
      content: [{
        type: 'text' as const,
        text: formatAsAsciiChart(data),
      }],
    };
  },
});

Critical Rules

  1. Tool's resourceUri must match a registered resource URI -- if the resource URI is app:///my-app, the tool must reference exactly app:///my-app.
  2. Always include content array with text fallback -- non-UI hosts (terminal CLIs, basic chat clients) need a text representation.
  3. CSP goes in contents[] of the resource read callback -- NOT in _meta on the tool.
  4. Use RESOURCE_MIME_TYPE constant -- never hardcode the MIME type string.

Verification Checklist

  • registerAppTool called with resourceUri matching a registered resource
  • registerAppResource called with matching URI and RESOURCE_MIME_TYPE
  • Tool handler returns content array with text fallback
  • Tool handler returns structuredContent for UI data
  • RESOURCE_MIME_TYPE imported and used (not hardcoded string)
  • App-only tools use visibility: ['app']
  • Resource read callback returns contents[] with CSP if needed
  • Multiple tools sharing a resource use the same URI

Task Definition

const mcpToolResourcePatternTask = defineTask({
  name: 'mcp-tool-resource-pattern',
  description: 'Implement Tool + Resource pattern for MCP App',

  inputs: {
    tools: { type: 'array', required: true },
    resourceUri: { type: 'string', required: true },
    appOnlyTools: { type: 'array', default: [] },
    cspDomains: { type: 'object', default: {} }
  },

  outputs: {
    toolsRegistered: { type: 'number' },
    resourceRegistered: { type: 'boolean' },
    artifacts: { type: 'array' }
  },

  async run(inputs, taskCtx) {
    return {
      kind: 'skill',
      title: `Implement Tool + Resource pattern (${inputs.tools.length} tools)`,
      skill: {
        name: 'mcp-tool-resource-pattern',
        context: {
          tools: inputs.tools,
          resourceUri: inputs.resourceUri,
          appOnlyTools: inputs.appOnlyTools,
          cspDomains: inputs.cspDomains,
          instructions: [
            'Register resource with RESOURCE_MIME_TYPE and bundled HTML',
            'Register each tool with resourceUri linking to the resource',
            'Include text content fallback in every tool handler',
            'Pass rich data via structuredContent',
            'Create app-only helper tools with visibility: [app]',
            'Configure CSP in contents[] if external origins needed'
          ]
        }
      },
      io: {
        inputJsonPath: `tasks/${taskCtx.effectId}/input.json`,
        outputJsonPath: `tasks/${taskCtx.effectId}/result.json`
      }
    };
  }
});

Applicable Processes

  • create-mcp-app.js
  • add-app-to-mcp-server.js
  • convert-web-app-to-mcp.js
  • migrate-openai-app-to-mcp.js

External Dependencies

  • @modelcontextprotocol/ext-apps (registerAppTool, registerAppResource, RESOURCE_MIME_TYPE)
  • @modelcontextprotocol/sdk (McpServer)
  • zod (input schema validation)

References

Related Skills

  • mcp-app-scaffolding
  • mcp-host-styling-integration
  • mcp-csp-investigation
  • single-file-bundling
  • mcp-app-verification

Related Agents

  • mcp-app-architect
  • mcp-ui-developer

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Provide implementations of advanced data structures

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

Fresh adversarial code review with binary PASS/FAIL verdicts, evidence citations, and anchoring bias prevention via fresh reviewer spawning.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

WASM-based instant code transforms for simple tasks, achieving 352x speedup over LLM inference with zero cost.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

Coordinate Crew (persistent) and Polecat (transient) agents using Gas Town's hook-based work distribution and GUPP principle.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

Generate visual representations of algorithm execution

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

Hierarchical coordination and drift detection with frequent checkpoints, shared memory coherence validation, role specialization enforcement, and short task cycles.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

a5c-ai/babysitter1.9천2026년 9월 17일 업데이트

a5c-ai의 스킬 모두 보기

이 스킬의 문제 신고