Skip to main content
Looking for the MCP Standard? This page covers the ChatGPT Apps SDK protocol specifically. For the official MCP Apps protocol (recommended), see MCP Apps.Cross-Compatibility: mcp-use uniquely allows you to write widgets once using type: "mcpApps" that work with both ChatGPT (Apps SDK) and MCP Apps clients automatically.
The ChatGPT Apps SDK is OpenAI’s proprietary protocol for interactive widgets in ChatGPT. While it’s specific to OpenAI’s ecosystem, mcp-use supports it fully alongside the standard MCP Apps protocol.

Protocol Choice: Apps SDK vs MCP Apps

Before starting, understand your options:
OptionProtocolCompatibilityUse When
type: "mcpApps"Both MCP Apps + ChatGPT✅ ChatGPT
✅ MCP Apps clients
You want maximum compatibility
type: "appsSdk"ChatGPT Apps SDK only✅ ChatGPT
❌ MCP Apps clients
ChatGPT-only features needed
Recommended: Use type: "mcpApps" for new projects to support both ChatGPT and MCP Apps clients. Your widget code remains identical—only the registration differs.See MCP Apps for details on the dual-protocol approach.

Quick Start

Start with the Apps SDK template which includes automatic widget registration:
npx create-mcp-use-app my-mcp-server --template mcp-apps
cd my-mcp-server
npm install
npm run dev
This creates a project with:
my-mcp-server/
├── resources/                      # 👈 Put your UI widgets here
│   ├── product-search-result/      # Folder-based widget example
│   │   ├── widget.tsx              # Widget entry point
│   │   ├── components/             # Reusable components
│   │   ├── hooks/                  # Custom hooks
│   │   ├── constants.ts
│   │   └── types.ts
│   └── styles.css                  # Global widget styles
├── public/                         # Static assets
│   └── fruits/                     # Example images
├── index.ts                        # Server entry point
├── package.json
├── tsconfig.json
└── README.md

Folder Structure

Widgets can be organized in two ways: single-file widgets or folder-based widgets. Choose the organization style that best fits your widget’s complexity. For simple widgets, a single file is sufficient:
resources/
├── user-card.tsx          # Widget file
├── weather-display.tsx    # Another widget
└── product-card.tsx       # Yet another widget
Each .tsx file in the resources/ folder becomes a widget. The widget name is derived from the filename (without extension). For complex widgets with multiple components, hooks, or utilities, organize them in folders:
resources/
├── widget-name.tsx                    # Single-file widget
└── product-search-result/             # Folder-based widget
    ├── widget.tsx                     # Entry point (required)
    ├── components/                    # Sub-components
    │   ├── Accordion.tsx
    │   ├── AccordionItem.tsx
    │   ├── Carousel.tsx
    │   └── CarouselItem.tsx
    ├── hooks/                         # Custom hooks
    │   └── useCarouselAnimation.ts
    ├── constants.ts                   # Constants
    └── types.ts                       # Type definitions
Key Points:
  • The folder name becomes the widget name (e.g., product-search-result)
  • The entry point must be named widget.tsx (not index.tsx)
  • You can organize sub-components, hooks, utilities, and types within the folder
  • The widget.tsx file should export widgetMetadata and the default component
Example Widget:
// resources/product-search-result/widget.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { Accordion } from "./components/Accordion";
import { Carousel } from "./components/Carousel";
import type { ProductSearchResultProps } from "./types";
import { propSchema } from "./types";

export const widgetMetadata: WidgetMetadata = {
  description: "Display product search results with filtering",
  props: propSchema,
};

const ProductSearchResult: React.FC = () => {
  const { props } = useWidget<ProductSearchResultProps>();

  return (
    <McpUseProvider autoSize>
      <div>
        <Carousel />
        <Accordion items={props.items} />
      </div>
    </McpUseProvider>
  );
};

export default ProductSearchResult;
Important: Widget LifecycleWidgets render before the tool execution completes. On first render, props will be an empty object {} and isPending will be true. Always check isPending or use optional chaining when accessing props to avoid runtime errors.
const { props, isPending } = useWidget<MyProps>();

if (isPending) {
  return <LoadingSpinner />;
}

// Now safe to access props.city, props.temperature, etc.
See Widget Lifecycle for complete details and patterns.

Dual-Protocol Support

mcp-use uniquely supports both ChatGPT Apps SDK and MCP Apps protocols. Choose your registration type based on compatibility needs: Works with both ChatGPT and MCP Apps clients:
import { MCPServer } from "mcp-use/server";

const server = new MCPServer({ name: "my-server", version: "1.0.0" });

server.uiResource({
  type: "mcpApps", // 👈 Dual-protocol support
  name: "weather-display",
  htmlTemplate: `...`,
  metadata: {
    csp: { connectDomains: ["https://api.weather.com"] },
    prefersBorder: true,
    autoResize: true, // MCP Apps specific
    widgetDescription: "...", // ChatGPT specific
  },
});
This generates metadata for both protocols automatically. Your widget code stays identical.

Using type: "appsSdk" (ChatGPT only)

For ChatGPT-exclusive features or legacy compatibility:
server.uiResource({
  type: "appsSdk", // ChatGPT only
  name: "weather-display",
  htmlTemplate: `...`,
  appsSdkMetadata: {
    "openai/widgetCSP": {
      connect_domains: ["https://api.weather.com"], // snake_case
      resource_domains: ["https://cdn.weather.com"],
    },
    "openai/widgetPrefersBorder": true,
    "openai/widgetDescription": "Displays current weather",
  },
});
Apps SDK Format: When using type: "appsSdk", you must use the appsSdkMetadata field (not metadata) with:
  • "openai/" prefixed keys
  • snake_case CSP fields (connect_domains, resource_domains)
For the simplified metadata format with camelCase, use type: "mcpApps" instead.
See MCP Apps for complete dual-protocol documentation and Content Security Policy for CSP configuration.

Widget Metadata

Contains the information that the MCP resource (and the tool that exposes it) will use when are automatically built by mcp-use.
import type { WidgetMetadata } from 'mcp-use/react';

export const widgetMetadata: WidgetMetadata = {
  // Required: Human-readable description
  description: string,

  // Required: Zod schema defining the widget's props shape
  props: z.ZodObject<...>,

  // Optional: Control automatic tool registration (defaults to false)
  exposeAsTool?: boolean,

  // Recommended: Unified metadata (works with both MCP Apps and ChatGPT)
  metadata?: {
    csp?: { connectDomains?: string[], resourceDomains?: string[] },
    prefersBorder?: boolean,
    invoking?: string,  // Status text while tool runs — auto-default: "Loading {name}..."
    invoked?: string,   // Status text after tool completes — auto-default: "{name} ready"
  },

  // Alternative: Apps SDK metadata (ChatGPT-only, legacy format)
  appsSdkMetadata?: {
    'openai/widgetCSP'?: { connect_domains?: string[], resource_domains?: string[] },
    'openai/widgetDescription'?: string,
    'openai/widgetAccessible'?: boolean,
  },
}
The props field expects a Zod schema that defines the shape of structuredContent returned by the tool. This schema is used to generate the tool’s input validation and to type-check the data in your widget via useWidget<T>().
Apps SDK Metadata Defaults: Widget templates automatically receive default Apps SDK metadata values:
  • openai/widgetDescription: Generated from your widget’s description
  • openai/widgetDomain: Defaults to "https://chatgpt.com" (required for app submission)
  • openai/widgetAccessible: Allows component-initiated tool calls
  • openai/widgetCSP: Content Security Policy with default trusted domains
  • openai/toolInvocation/invoking: Loading state text — set via metadata.invoking (default: "Loading MyWidget...")
  • openai/toolInvocation/invoked: Ready state text — set via metadata.invoked (default: "MyWidget ready")
These status texts are displayed as animated shimmer text (pending) and static text (complete) in the MCP Inspector. You can override them via metadata.invoking/metadata.invoked in your widget registration, or use appsSdkMetadata for raw key access: You can customize any of these in your widget’s appsSdkMetadata export:
export const appsSdkMetadata = {
  "openai/widgetDomain": "https://custom-domain.com", // Override default
  "openai/widgetPrefersBorder": true,
  "openai/widgetCSP": {
    connect_domains: ["https://api.example.com"],
    resource_domains: ["https://cdn.example.com"],
  },
};
Widget Exposure Modes:
Breaking change: exposeAsTool now defaults to false. Widgets are registered as MCP resources only. To make a widget callable directly by a model, either set exposeAsTool: true in your widget metadata, or define a custom tool that returns the widget using the widget() helper (recommended).
By default, widgets are registered as MCP resources only — they are not auto-registered as tools. The recommended pattern is to expose widgets through a custom tool:
  • Via custom tool (recommended): Define a tool that calls widget() in its handler. This gives you full control over tool naming, description, parameters, and business logic:
    // resources/weather-display/widget.tsx — no exposeAsTool needed
    export const widgetMetadata: WidgetMetadata = {
      description: "Display weather",
      props: propSchema,
    };
    
    // index.ts — expose via a custom tool
    server.tool({
      name: "get-weather",
      description: "Get current weather for a city",
      schema: z.object({ city: z.string() }),
      widget: { name: "weather-display", invoking: "Fetching weather...", invoked: "Weather loaded" },
    }, async ({ city }) => {
      const data = await fetchWeather(city);
      return widget({ props: data, output: text(`Weather in ${city}: ${data.temp}°C`) });
    });
    
  • Auto-registered: Widget is auto-registered as a tool using its metadata description and props schema as the tool’s parameters. Simpler, but less control:
    export const widgetMetadata: WidgetMetadata = {
      description: "Display weather",
      props: propSchema,
      exposeAsTool: true, // Opt in to auto-registration
    };
    
Note: Your CSP domains are merged with your server’s base URL automatically. For ChatGPT, OpenAI’s required domains (*.oaistatic.com, etc.) are also added. For MCP Apps clients, only the domains you declare are used. See Content Security Policy for full CSP documentation.

Components & Hooks

mcp-use provides a comprehensive set of React components and hooks for building OpenAI Apps SDK widgets. These components handle common setup tasks like theme management, error handling, routing, and debugging.

Components

ComponentDescriptionLink
McpUseProviderUnified provider that combines all common React setup (StrictMode, ThemeProvider, WidgetControls, ErrorBoundary)McpUseProvider →
WidgetControlsDebug button and view controls (fullscreen/pip) with customizable positioningWidgetControls →
ErrorBoundaryError boundary component for graceful error handling in widgetsErrorBoundary →
ImageImage component that handles both data URLs and public file pathsImage →
ThemeProviderTheme provider for consistent theme management across widgetsThemeProvider →

Hooks

HookDescriptionLink
useWidgetMain hook providing type-safe access to all widget capabilities (props, state, theme, actions)useWidget →

Static Assets

Widgets can use static assets from a public/ folder. The framework automatically serves these assets and copies them during build. Folder Structure
my-mcp-server/
├── resources/
│   └── product-widget.tsx
├── public/                    # Static assets
│   ├── fruits/
│   │   ├── apple.png
│   │   ├── banana.png
│   │   └── orange.png
│   └── logo.svg
└── index.ts
Using Public Assets In development, assets are served from /mcp-use/public/. In production, they’re copied to dist/public/ during build. Using the Image Component:
import { Image } from "mcp-use/react";

function ProductWidget() {
  return (
    <div>
      {/* Paths are relative to public/ folder */}
      <Image src="/fruits/apple.png" alt="Apple" />
      <Image src="/logo.svg" alt="Logo" />
    </div>
  );
}
Direct URL Access: The framework provides utilities for accessing public files:
  • window.__mcpPublicUrl: Base URL for public assets (e.g., http://localhost:3000/mcp-use/public)
  • window.__getFile: Helper function to get file URLs
// Get public URL
const publicUrl = window.__mcpPublicUrl; // "http://localhost:3000/mcp-use/public"
const imageUrl = `${publicUrl}/fruits/apple.png`;

// Or use the helper
const imageUrl = window.__getFile?.("fruits/apple.png");

Patterns

Accessing Widget Data

The useWidget hook provides access to both the server-computed props and the original tool arguments:
const { props, toolInput, isPending } = useWidget();
  • props — Data computed by the server, sent via structuredContent (the LLM does not see this)
  • toolInput — The original tool call arguments from the model (e.g. { query: "mango" })
Example:
export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  props: z.object({
    query: z.string(),
    results: z.array(z.object({ name: z.string() })),
  }),
};

const ProductSearchResult: React.FC = () => {
  const { props, toolInput, isPending } = useWidget();

  if (isPending) return <div>Loading...</div>;

  return (
    <div>
      <p>You searched for: {toolInput.query}</p>
      <p>Found {props.results.length} results</p>
    </div>
  );
};

Widget Lifecycle and Loading States

Widgets render before the tool execution completes. This allows you to show loading states while data is being fetched. The isPending flag indicates when the tool is still executing:
  • isPending = true: Widget just mounted, props is empty {}
  • isPending = false: Tool completed, props contains actual data
When the host supports it (MCP Apps, MCP Inspector), you can also show streaming tool arguments in real time via partialToolInput and isStreaming from useWidget for live previews while the LLM generates the tool call. See Streaming tool arguments in the useWidget docs. Pattern 1: Early return with loading state
const MyWidget: React.FC = () => {
  const { props, isPending } = useWidget<MyWidgetProps>();

  if (isPending) {
    return (
      <div className="flex items-center justify-center p-8">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
      </div>
    );
  }

  // Safe to access props.city, props.temperature, etc.
  return (
    <div>
      <h1>{props.city}</h1>
      <p>{props.temperature}°C</p>
    </div>
  );
};
Pattern 2: Conditional rendering
const MyWidget: React.FC = () => {
  const { props, isPending } = useWidget<MyWidgetProps>();

  return (
    <div>
      {isPending ? (
        <LoadingSpinner />
      ) : (
        <div>
          <h1>{props.city}</h1>
          <p>{props.temperature}°C</p>
        </div>
      )}
    </div>
  );
};
Pattern 3: Using optional chaining (when you want to show partial UI)
const MyWidget: React.FC = () => {
  const { props } = useWidget<MyWidgetProps>();

  return (
    <div>
      <h1>{props.city ?? "Loading..."}</h1>
      <p>{props.temperature ? `${props.temperature}°C` : "Loading..."}</p>
    </div>
  );
};

Widget State

Widgets can maintain state across interactions. State is persisted by the host, for example in ChatGPT:
const { state, setState } = useWidget();

// Save state (persists to localStorage)
await setState({ favoriteCity: "Tokyo", filters: { price: "$$" } });

// State persists across widget re-renders and page reloads
console.log(state?.favoriteCity); // 'Tokyo'

// Update state with function (like React useState)
await setState((prev) => ({
  ...prev,
  favoriteCity: "Paris",
}));

Calling Other Tools

Widgets can call MCP tools to perform actions and fetch data. Use the dedicated useCallTool hook for the best developer experience with automatic type inference and state management.
Automatic Type Inference: When you run mcp-use dev, types are automatically generated from your tool definitions. This gives you autocomplete for tool names, inputs, and outputs.
The useCallTool hook provides TanStack Query-like state management with full TypeScript support:
import { useWidget, useCallTool } from "mcp-use/react";

const InteractiveWidget = () => {
  const { props } = useWidget();
  const { callTool, data, isPending, isSuccess, isError, error } = 
    useCallTool("search-flights");

  const handleSearch = () => {
    callTool({ destination: "NYC", date: "2024-12-01" });
  };

  return (
    <div>
      <button onClick={handleSearch} disabled={isPending}>
        {isPending ? "Searching..." : "Search Flights"}
      </button>
      {isSuccess && (
        <ul>
          {data.structuredContent.flights.map(flight => (
            <li key={flight.id}>{flight.price}</li>
          ))}
        </ul>
      )}
      {isError && <p>Error: {error.message}</p>}
    </div>
  );
};
Benefits:
  • Type-safe: Autocomplete for tool names, inputs, and outputs
  • State management: Built-in loading, success, and error states
  • Callbacks: Optional onSuccess, onError, and onSettled handlers
  • Async/await: Use callToolAsync for promise-based flows
See useCallTool() for complete documentation.

Type Generation

Types are automatically generated during development:
# Types are generated automatically
mcp-use dev

# Or generate manually
mcp-use generate-types
This creates .mcp-use/tool-registry.d.ts with type definitions for all your tools, enabling full IntelliSense in your widgets.

Using useWidget.callTool (Alternative)

You can also call tools directly from useWidget():
const { callTool } = useWidget();

const handleSearch = async () => {
  try {
    const result = await callTool("search_cities", {
      query: "tokyo",
    });
    // result.content contains the tool response
    console.log(result.content);
  } catch (error) {
    console.error("Tool call failed:", error);
  }
};
While useWidget().callTool works, useCallTool is recommended for better type safety and state management.

Display Mode Control

Request different display modes (inline, pip, or fullscreen):
const { requestDisplayMode, displayMode } = useWidget();

const handleExpand = async () => {
  const result = await requestDisplayMode("fullscreen");
  // result.mode = 'fullscreen' (may be different if request denied)
  console.log("Display mode:", result.mode);
};

// Current display mode
console.log(displayMode); // 'inline' | 'pip' | 'fullscreen'
Display Modes:
  • 'inline' - Default embedded view in conversation
  • 'pip' - Picture-in-Picture floating window
  • 'fullscreen' - Full browser window (on mobile, PiP coerces to fullscreen)
You can user the <WidgetControls /> to automatically add controls to your widget.
import { WidgetControls } from "mcp-use/react";

function MyWidget() {
  return (
    <McpUseProvider viewControls>
      <div>My widget content</div>
    </McpUseProvider>
  );
}
or
import { WidgetControls } from "mcp-use/react";

function MyWidget() {
  return (
    <WidgetControls>
      <div>My widget content</div>
    </WidgetControls>
  );
}
Widgets run inside sandboxed iframes, which block two common patterns:
  • Blob + URL.createObjectURL() — object URLs cannot cross the iframe boundary
  • element.requestFullscreen() — the browser Fullscreen API is blocked in sandboxed iframes (use requestDisplayMode("fullscreen") instead)
Use openExternal from useWidget() for all outbound navigation. For file downloads, serve the file from the MCP server and open the URL externally:
Never use new Blob(...) + URL.createObjectURL() or window.open() inside a widget. Both are blocked by the iframe sandbox.
import { useWidget } from "mcp-use/react";

function DownloadButton({ svgData }: { svgData: string }) {
  const { mcp_url, openExternal } = useWidget();

  const handleDownload = async () => {
    // POST the content to a server-side endpoint
    const res = await fetch(`${mcp_url}/api/download`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ data: svgData, filename: "export.svg" }),
    });
    const { id } = await res.json();

    // Open the resulting URL — host handles the actual download
    openExternal(`${mcp_url}/api/download/${id}`);
  };

  return <button onClick={handleDownload}>Download SVG</button>;
}
Your MCP server needs to expose matching endpoints:
// POST /api/download — store content, return an ID
// GET  /api/download/:id — serve with Content-Disposition: attachment

Custom Tools with Widgets

You can create custom tools that return widgets instead of relying solely on automatic widget registration. This is useful when you need to:
  • Fetch data before displaying the widget
  • Use different tool parameters than widget props
  • Have multiple tools use the same widget
  • Add custom logic or validation

Using the widget() Helper

The widget() helper returns runtime data for a widget. You must combine it with the widget config on the tool definition to set up all registration-time metadata. Important: The widget configuration is split between two places:
  1. Tool definition (widget: { name, invoking, ... }) - Registration-time metadata
  2. Helper return (widget({ props, output, ... })) - Runtime data
import { widget, text } from "mcp-use/server";
import { z } from "zod";

server.tool(
  {
    name: "get-weather",
    description: "Get current weather for a city",
    schema: z.object({
      city: z.string().describe("City name"),
    }),
    // Widget config sets all registration-time metadata
    widget: {
      name: "weather-display", // Must match a widget in resources/
      invoking: "Fetching weather data...",
      invoked: "Weather data loaded",
    },
  },
  async ({ city }) => {
    // Fetch weather data from API
    const weatherData = await fetchWeatherAPI(city);

    // Return widget with runtime data only
    return widget({
      props: {
        city,
        temperature: weatherData.temp,
        conditions: weatherData.conditions,
        humidity: weatherData.humidity,
        windSpeed: weatherData.windSpeed,
      },
      output: text(
        `Current weather in ${city}: ${weatherData.temp}°C, ${weatherData.conditions}`,
      ),
      message: `Current weather in ${city}`,
    });
  },
);
Key Points:
  • widget: { name, invoking, invoked, ... } on tool definition - Configures all widget metadata at registration time
  • widget({ props, output, metadata, message }) helper - Returns runtime data only:
    • props - Widget rendering data sent as structuredContent (LLM does not see). Widget reads via useWidget().props.
    • output - Optional response helper (text(), object(), etc.) that the model sees in content
    • metadata - Optional extra data in _meta. Widget reads via useWidget().metadata.
    • message - Optional text message override for content
  • Widget must exist - The widget name must match a .tsx file or folder in resources/
  • exposeAsTool defaults to false - Widgets are resources by default. This is the expected setup when returning widgets from custom tools. To opt into auto-registration instead, set exposeAsTool: true in the widget metadata:
// resources/weather-display/widget.tsx
export const widgetMetadata: WidgetMetadata = {
  description: "Display weather information",
  props: propSchema,
  exposeAsTool: true, // Opt in to auto-register as a tool
};
See Tools Guide for more information about the widget config option.

Configuration

Base URL for Production

Set the MCP_URL environment variable or pass baseUrl:
const server = new MCPServer({
  name: "my-server",
  version: "1.0.0",
  baseUrl: process.env.MCP_URL || "https://myserver.com",
});
This ensures:
  • Widget URLs use the correct domain
  • Apps SDK CSP automatically includes your server
Preferably set the variable at build time to have statically generated widget assets paths. If you don’t set it at build time you must set it at runtime either by passing the baseUrl option to the MCPServer constructor or by setting the MCP_URL environment variable.

Environment Variables

# Server Configuration
MCP_URL=https://myserver.com

# For Static Deployments (e.g., Supabase)
MCP_SERVER_URL=https://myserver.com/functions/v1/my-function
CSP_URLS=https://myserver.com,...other domains
Environment Variable Details:
  • MCP_URL: Base URL for widget assets and public files. Used by Vite’s base option during build. Also used by the server to configure CSP.
  • MCP_SERVER_URL: (Optional) MCP server URL for API calls. When set, URLs are injected at build time for static deployments where widgets are served from storage rather than the MCP server.
  • CSP_URLS: (Optional) Additional domains to whitelist in widget Content Security Policy. Supports comma-separated list. For Supabase, use the base project URL without path (e.g., https://nnpumlykjksvxivhywwo.supabase.co). Required for static deployments where widget assets are served from different domains. See Content Security Policy for full CSP documentation.
Static Deployments: Set MCP_URL (for assets), MCP_SERVER_URL (for API calls), and CSP_URLS (for CSP whitelisting) when deploying to platforms like Supabase where widgets are served from static storage.Alternative CSP Configuration: Instead of using the global CSP_URLS environment variable, you can configure CSP per-widget in your widget’s appsSdkMetadata['openai/widgetCSP'] (see Apps SDK Metadata or Content Security Policy).

Testing

Using the Inspector

The mcp-use Inspector provides full support for testing widgets during development:
  1. Start your server: npm run dev
  2. Open Inspector: http://localhost:3000/inspector
  3. Test widgets: Execute tools to see widgets render
  4. Verify lifecycle: Widgets render immediately with isPending=true, then update when tool completes
  5. Debug interactions: Use console logs and inspector features
  6. Test API methods: Verify callTool, setState, etc. work correctly
See Debugging Widgets for complete testing guide.

Testing in ChatGPT

You need to enable the Developer Mode in ChatGPT to test widgets.
  • Enable developer mode: Go to Settings → Connectors → Advanced → Developer mode.
  • Import MCPs: In the Connectors tab, add your remote MCP server. It will appear in the composer’s “Developer Mode” tool later during conversations.
  • Use connectors in conversations: Choose Developer mode from the Plus menu and select connectors. You may need to explore different prompting techniques to call the correct tools. For example:
    • Be explicit: “Use the “Acme CRM” connector’s “update_record” tool to …”. When needed, include the server label and tool name.
    • Disallow alternatives to avoid ambiguity: “Do not use built-in browsing or other tools; only use the Acme CRM connector.”
    • Disambiguate similar tools: “Prefer Calendar.create_event for meetings; do not use Reminders.create_task for scheduling.”
    • Specify input shape and sequencing: “First call Repo.read_file with { path: ”…” }. Then call Repo.write_file with the modified content. Do not call other tools.”
    • If multiple connectors overlap, state preferences up front (e.g., “Use CompanyDB for authoritative data; use other sources only if CompanyDB returns no results”).
    • Developer mode does not require search/fetch tools. Any tools your connector exposes (including write actions) are available, subject to confirmation settings.
    • See more guidance in Using tools and Prompting.
    • Improve tool selection with better tool descriptions: In your MCP server, write action-oriented tool names and descriptions that include “Use this when…” guidance, note disallowed/edge cases, and add parameter descriptions (and enums) to help the model choose the right tool among similar ones and avoid built-in tools when inappropriate.

Next Steps