Back to Skills

OpenAI Apps MCP

jezweb
Updated Yesterday
49 views
33
4
33
View on GitHub
Metawordaimcpdesigndata

About

This skill enables developers to build ChatGPT apps using stateless MCP servers deployed on Cloudflare Workers. It provides implementation guidance for the MCP protocol, OpenAI metadata formats, and widget serving patterns while preventing common deployment errors. Use it when creating tools that extend ChatGPT functionality or serve interactive widgets within OpenAI's ecosystem.

Quick Install

Claude Code

Recommended
Plugin CommandRecommended
/plugin add https://github.com/jezweb/claude-skills
Git CloneAlternative
git clone https://github.com/jezweb/claude-skills.git ~/.claude/skills/OpenAI Apps MCP

Copy and paste this command in Claude Code to install this skill

Documentation

Building OpenAI Apps with Stateless MCP Servers

Status: Production Ready Last Updated: 2025-11-17 Dependencies: cloudflare-worker-base, hono-routing (optional, helpful for routing patterns) Latest Versions: @modelcontextprotocol/[email protected], [email protected], [email protected]


Overview

This skill provides production-tested patterns for building OpenAI Apps - applications that extend ChatGPT's functionality through the Model Context Protocol (MCP). Focus on stateless MCP servers using Cloudflare Workers, which covers 80% of OpenAI Apps use cases.

What Are OpenAI Apps?

OpenAI Apps are extensions that integrate into the ChatGPT interface, allowing users to:

  • Access third-party services directly in conversations
  • Display interactive widgets (maps, carousels, lists, etc.)
  • Execute tools that return structured UI components
  • Enhance ChatGPT with domain-specific capabilities

Architecture

ChatGPT User
    ↓
ChatGPT (discovers and invokes tools)
    ↓
MCP Server (your Cloudflare Worker)
    ├── Tool handlers (business logic)
    ├── Widget resources (HTML/JS UI)
    └── OpenAI metadata (output templates)

Key Components

  1. MCP Server - HTTP endpoint exposing tools via Model Context Protocol
  2. Tool Handlers - Functions that process inputs and return results
  3. Widget Resources - HTML pages that render in ChatGPT's iframe
  4. OpenAI Metadata - Special annotations for widget routing and display

Quick Start (10 Minutes)

1. Scaffold Project

npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
cd my-openai-app

# Install dependencies
npm install @modelcontextprotocol/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]

Why this matters:

  • @modelcontextprotocol/sdk is the official MCP protocol implementation
  • hono provides lightweight routing perfect for API endpoints
  • Vite + CloudFlare plugin enable building and serving widgets

2. Configure wrangler.jsonc

{
  "name": "my-openai-app",
  "main": "dist/index.js",
  "compatibility_date": "2025-10-08",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": "dist/client",
    "binding": "ASSETS"
  },
  "observability": {
    "enabled": true
  }
}

CRITICAL:

  • nodejs_compat flag is required for MCP SDK
  • assets.binding: "ASSETS" must match TypeScript binding name
  • assets.directory must match Vite build output

3. Create MCP Server

// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

type Bindings = {
  ASSETS: Fetcher;
};

const app = new Hono<{ Bindings: Bindings }>();

// CORS - must allow ChatGPT
app.use('/mcp/*', cors({
  origin: 'https://chatgpt.com',
  credentials: true,
  allowMethods: ['GET', 'POST', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization']
}));

// Create MCP server
const mcpServer = new Server(
  { name: 'my-openai-app', version: '1.0.0' },
  { capabilities: { tools: {}, resources: {} } }
);

// Register a simple tool
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'hello_world',
    description: 'Use this when the user wants to see a hello world message',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Name to greet' }
      },
      required: ['name']
    },
    annotations: {
      openai: {
        outputTemplate: 'ui://widget/hello.html'
      }
    }
  }]
}));

mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'hello_world') {
    const { name } = request.params.arguments as { name: string };
    return {
      content: [{ type: 'text', text: `Hello, ${name}!` }],
      _meta: { initialData: { name } }
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

// MCP endpoint
app.post('/mcp', async (c) => {
  const body = await c.req.json();
  const response = await mcpServer.handleRequest(body);
  return c.json(response);
});

// Serve widgets
app.get('/widgets/*', async (c) => c.env.ASSETS.fetch(c.req.raw));

export default app;

The 5-Step Setup Process

Step 1: Project Scaffolding

Use Cloudflare's official scaffolding:

npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false

Key Points:

  • Creates Workers project with TypeScript
  • Includes wrangler.jsonc
  • Initializes git repository

Step 2: Install Dependencies

npm install @modelcontextprotocol/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]

What each package does:

  • @modelcontextprotocol/sdk - Official MCP protocol (Anthropic)
  • hono - Fast, lightweight routing framework
  • zod - Runtime type validation for tool inputs
  • @cloudflare/vite-plugin - Build tool for Workers + static assets
  • vite - Frontend build tool

Step 3: Configure Build System

Create vite.config.ts:

import { defineConfig } from 'vite';
import { cloudflareDevProxyVitePlugin as cloudflare } from '@cloudflare/vite-plugin';

export default defineConfig({
  plugins: [
    cloudflare({
      configPath: 'wrangler.jsonc',
      persist: { path: '.wrangler/state' }
    })
  ],
  build: {
    outDir: 'dist',
    rollupOptions: {
      input: {
        worker: './src/index.ts'
      },
      output: {
        entryFileNames: (chunkInfo) => {
          if (chunkInfo.name === 'worker') return 'index.js';
          return 'client/[name]-[hash].js';
        }
      }
    }
  }
});

Why this matters:

  • Builds both worker code and static assets
  • Proper output structure for Workers + ASSETS binding
  • Content hashing for cache busting

Step 4: Create Widget HTML

mkdir -p src/widgets

Create src/widgets/hello.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hello Widget</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: var(--background);
      color: var(--foreground);
    }
    .greeting {
      font-size: 24px;
      font-weight: 600;
    }
  </style>
</head>
<body>
  <div class="greeting" id="greeting">Loading...</div>

  <script>
    // Access initial data from tool handler
    if (window.openai && window.openai.getInitialData) {
      const data = window.openai.getInitialData();
      document.getElementById('greeting').textContent = `Hello, ${data.name}! 👋`;
    }
  </script>
</body>
</html>

What to avoid:

  • Don't use third-party CDN scripts (CSP may block)
  • Don't use custom fonts (use system fonts)
  • Don't make external API calls without CORS

Step 5: Deploy and Test

# Build
npm run build

# Deploy to Cloudflare
npx wrangler deploy

# Test with MCP Inspector
npx @modelcontextprotocol/inspector https://my-openai-app.workers.dev/mcp

Critical Rules

Always Do

✅ Set CORS to allow https://chatgpt.com ✅ Use resource URI pattern ui://widget/ for widgets ✅ Set MIME type to text/html+skybridge for HTML resources ✅ Include _meta.initialData in tool responses for widget initialization ✅ Use action-oriented tool descriptions ("Use this when...") ✅ Validate tool inputs with Zod schemas ✅ Test with MCP Inspector before deploying to ChatGPT

Never Do

❌ Use custom MIME types (must be text/html+skybridge) ❌ Forget CORS configuration (ChatGPT won't connect) ❌ Use resource URIs without ui://widget/ prefix ❌ Bundle widgets in worker code (use ASSETS binding) ❌ Skip input validation (tools receive untrusted data) ❌ Use SSE without heartbeat (100s timeout on Workers) ❌ Deploy without testing in MCP Inspector first


Known Issues Prevention

This skill prevents 8 documented issues:

Issue #1: CORS Policy Blocks MCP Endpoint

Error: Access to fetch at 'https://my-app.workers.dev/mcp' from origin 'https://chatgpt.com' has been blocked by CORS policy

Source: Common browser console error when CORS not configured

Why It Happens: ChatGPT makes cross-origin requests to your MCP server. Without CORS headers, browsers block these requests.

Prevention:

app.use('/mcp/*', cors({
  origin: 'https://chatgpt.com',  // Must allow ChatGPT specifically
  credentials: true,
  allowMethods: ['GET', 'POST', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization']
}));

Issue #2: Widget Returns 404 Not Found

Error: Failed to load resource: the server responded with a status of 404 (Not Found) for widget URL

Source: OpenAI Apps SDK requirement for ui://widget/ prefix

Why It Happens: Resource URI must follow specific pattern. Wrong prefix causes ChatGPT to fail widget resolution.

Prevention:

// ✅ Correct
annotations: {
  openai: {
    outputTemplate: 'ui://widget/map.html'
  }
}

// ❌ Wrong - will 404
outputTemplate: 'resource://map.html'
outputTemplate: '/widgets/map.html'

Issue #3: Widget Displays as Plain Text

Error: HTML source code visible instead of rendered widget

Source: OpenAI Apps SDK MIME type requirement

Why It Happens: ChatGPT expects specific MIME type text/html+skybridge to render widgets. Standard text/html won't work.

Prevention:

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [{
    uri: 'ui://widget/map.html',
    mimeType: 'text/html+skybridge'  // Must use +skybridge
  }]
}));

Issue #4: ASSETS Binding Undefined

Error: TypeError: Cannot read property 'fetch' of undefined when accessing c.env.ASSETS

Source: Misconfigured wrangler.jsonc or TypeScript types

Why It Happens: ASSETS binding name in wrangler.jsonc must match TypeScript binding interface.

Prevention:

// wrangler.jsonc
{
  "assets": {
    "directory": "dist/client",
    "binding": "ASSETS"  // Must match TypeScript
  }
}
// src/index.ts
type Bindings = {
  ASSETS: Fetcher;  // Must match wrangler.jsonc binding name
};

Issue #5: SSE Connection Drops After 100 Seconds

Error: SSE stream closes unexpectedly, tools stop responding

Source: Cloudflare Workers SSE timeout when no data sent

Why It Happens: Workers close SSE connections after 100 seconds of inactivity. Must send heartbeat events.

Prevention:

import { streamSSE } from 'hono/streaming';

app.get('/mcp/stream', (c) => {
  return streamSSE(c, async (stream) => {
    // Send heartbeat every 30 seconds
    const heartbeat = setInterval(async () => {
      await stream.writeSSE({
        data: JSON.stringify({ type: 'heartbeat' }),
        event: 'ping'
      });
    }, 30000);

    // Clean up on close
    stream.onAbort(() => clearInterval(heartbeat));
  });
});

Issue #6: ChatGPT Doesn't Suggest Tool

Error: Tool registered but never appears in ChatGPT suggestions

Source: OpenAI tool discovery requires action-oriented descriptions

Why It Happens: Generic descriptions don't match user intent patterns. ChatGPT uses descriptions to decide when to invoke tools.

Prevention:

// ✅ Good - action-oriented
description: 'Use this when the user wants to see a location on a map'

// ❌ Bad - generic
description: 'Shows a map'

// ✅ Good - specific use case
description: 'Use this when the user asks about weather in a specific city'

// ❌ Bad - vague
description: 'Gets weather data'

Issue #7: Widget Can't Access Initial Data

Error: window.openai.getInitialData() returns undefined in widget

Source: Missing _meta.initialData in tool response

Why It Happens: Data must be explicitly passed via _meta field. Regular response content doesn't reach widgets.

Prevention:

// Tool handler
return {
  content: [{
    type: 'text',
    text: 'Here is your map'
  }],
  _meta: {
    initialData: {  // This object reaches window.openai.getInitialData()
      location: 'San Francisco',
      zoom: 12
    }
  }
};

Issue #8: Widget Scripts Blocked by CSP

Error: Refused to load the script because it violates the following Content Security Policy directive

Source: ChatGPT's Content Security Policy for widget iframes

Why It Happens: External scripts from third-party CDNs are blocked for security.

Prevention:

<!-- ✅ Inline scripts work -->
<script>
  console.log('Widget loaded');
</script>

<!-- ✅ Same-origin scripts work -->
<script src="/widgets/lib.js"></script>

<!-- ❌ Third-party CDNs may be blocked -->
<script src="https://cdn.example.com/library.js"></script>

<!-- Workaround: Bundle libraries with Vite -->

Configuration Files Reference

wrangler.jsonc (Full Example)

{
  "name": "my-openai-app",
  "main": "dist/index.js",
  "compatibility_date": "2025-10-08",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": "dist/client",
    "binding": "ASSETS"
  },
  "observability": {
    "enabled": true
  },
  "vars": {
    "ENVIRONMENT": "production"
  }
}

Why these settings:

  • nodejs_compat - Required for MCP SDK (uses Node.js APIs)
  • assets.directory - Where Vite outputs widget HTML files
  • assets.binding: "ASSETS" - How to access static assets in code
  • observability - Enable logging and tracing for debugging

vite.config.ts (Full Example)

import { defineConfig } from 'vite';
import { cloudflareDevProxyVitePlugin as cloudflare } from '@cloudflare/vite-plugin';

export default defineConfig({
  plugins: [
    cloudflare({
      configPath: 'wrangler.jsonc',
      persist: { path: '.wrangler/state' }
    })
  ],
  build: {
    outDir: 'dist',
    rollupOptions: {
      input: {
        worker: './src/index.ts',
        // Widget entry points
        hello: './src/widgets/hello.html',
        map: './src/widgets/map.html'
      },
      output: {
        entryFileNames: (chunkInfo) => {
          if (chunkInfo.name === 'worker') return 'index.js';
          return 'client/[name]-[hash].js';
        },
        assetFileNames: 'client/[name]-[hash][extname]'
      }
    }
  }
});

Why these settings:

  • Multiple input entry points (worker + widgets)
  • Separate output for worker (index.js) vs client assets (client/)
  • Content hashing for cache busting ([hash])

Common Patterns

Pattern 1: Simple Text-Only Tool

Tool that returns text without widgets:

mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'get_weather',
    description: 'Use this when the user asks about current weather in a city',
    inputSchema: {
      type: 'object',
      properties: {
        city: { type: 'string', description: 'City name' }
      },
      required: ['city']
    }
    // No annotations - returns plain text
  }]
}));

mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'get_weather') {
    const { city } = request.params.arguments as { city: string };

    // In production, call weather API
    const weather = await fetchWeather(city);

    return {
      content: [{
        type: 'text',
        text: `Weather in ${city}: ${weather.description}, ${weather.temp}°F`
      }]
    };
  }
});

When to use: Simple data retrieval that doesn't need visual representation

Pattern 2: List Widget

Tool that displays a list of items:

// Tool registration
{
  name: 'search_products',
  description: 'Use this when the user wants to search for products',
  inputSchema: {
    type: 'object',
    properties: {
      query: { type: 'string' }
    }
  },
  annotations: {
    openai: {
      outputTemplate: 'ui://widget/product-list.html'
    }
  }
}

// Tool handler
const products = await searchProducts(query);
return {
  content: [{
    type: 'text',
    text: `Found ${products.length} products`
  }],
  _meta: {
    initialData: {
      products: products.map(p => ({
        id: p.id,
        name: p.name,
        price: p.price,
        image: p.image
      }))
    }
  }
};
<!-- product-list.html -->
<div id="products"></div>
<script>
const data = window.openai.getInitialData();
const container = document.getElementById('products');

data.products.forEach(product => {
  const item = document.createElement('div');
  item.className = 'product-item';
  item.innerHTML = `
    <img src="${product.image}" alt="${product.name}">
    <h3>${product.name}</h3>
    <p>$${product.price}</p>
  `;
  container.appendChild(item);
});
</script>

When to use: Displaying collections of items (search results, recommendations, etc.)

Pattern 3: Interactive Widget with Tool Callbacks

Widget that can trigger other tools:

// Register multiple related tools
{
  name: 'show_tasks',
  description: 'Use this when user wants to see their task list',
  annotations: {
    openai: {
      outputTemplate: 'ui://widget/tasks.html'
    }
  }
},
{
  name: 'complete_task',
  description: 'Mark a task as completed',
  inputSchema: {
    type: 'object',
    properties: {
      taskId: { type: 'number' }
    }
  }
}
<!-- tasks.html -->
<div id="tasks"></div>
<script>
const data = window.openai.getInitialData();

data.tasks.forEach(task => {
  const taskEl = document.createElement('div');
  taskEl.innerHTML = `
    <span>${task.title}</span>
    <button onclick="completeTask(${task.id})">Complete</button>
  `;
  document.getElementById('tasks').appendChild(taskEl);
});

function completeTask(taskId) {
  if (window.openai && window.openai.callTool) {
    window.openai.callTool({
      name: 'complete_task',
      arguments: { taskId }
    });
  }
}
</script>

When to use: Interactive widgets that need to trigger follow-up actions


Using Bundled Resources

Scripts (scripts/)

Run ./scripts/scaffold-openai-app.sh to generate a complete starter project:

chmod +x ./scripts/scaffold-openai-app.sh
./scripts/scaffold-openai-app.sh my-new-app

cd my-new-app
npm install
npm run dev

References (references/)

  • references/mcp-protocol-basics.md - MCP SDK usage patterns
  • references/openai-metadata-format.md - All OpenAI-specific annotation fields
  • references/widget-patterns.md - Widget HTML examples and best practices
  • references/optional-agents-upgrade.md - When and how to add stateful features

When Claude should load these: When you need detailed API references or advanced patterns beyond this SKILL.md

Templates (templates/)

  • templates/basic/ - Minimal stateless MCP server
  • templates/with-react/ - React-based widgets with Vite build

Usage: Copy entire template directory as starting point for new projects


Advanced Topics

Using Zod for Input Validation

import { z } from 'zod';

const CreateTaskInputSchema = z.object({
  title: z.string().min(1).max(200),
  dueDate: z.string().datetime().optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium')
});

// Convert Zod schema to JSON Schema for MCP
const jsonSchema = zodToJsonSchema(CreateTaskInputSchema);

// Use in tool registration
{
  name: 'create_task',
  inputSchema: jsonSchema,
  // ...
}

// Validate in handler
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'create_task') {
    try {
      const validated = CreateTaskInputSchema.parse(request.params.arguments);
      // validated is typed and validated
    } catch (error) {
      if (error instanceof z.ZodError) {
        return {
          content: [{
            type: 'text',
            text: `Invalid input: ${error.errors.map(e => e.message).join(', ')}`
          }],
          isError: true
        };
      }
    }
  }
});

Environment Variables and Secrets

# Set secrets via Wrangler
wrangler secret put API_KEY

# Access in code
type Bindings = {
  ASSETS: Fetcher;
  API_KEY: string;  // Secret binding
};

// In handler
const apiKey = c.env.API_KEY;

React Widgets with Vite

// vite.config.ts - add React plugin
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    cloudflare({ /* ... */ })
  ],
  // ...
});
// src/widgets/TaskList.tsx
import { useState, useEffect } from 'react';

export function TaskList() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    if (window.openai) {
      const data = window.openai.getInitialData();
      setTasks(data.tasks);
    }
  }, []);

  return (
    <div>
      {tasks.map(task => (
        <div key={task.id}>{task.title}</div>
      ))}
    </div>
  );
}

Dependencies

Required:

Optional:


Official Documentation


Package Versions (Verified 2025-11-17)

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.21.0",
    "hono": "^4.10.2",
    "zod": "^3.25.76"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "^1.13.13",
    "@cloudflare/workers-types": "^4.20250531.0",
    "@vitejs/plugin-react": "^4.3.4",
    "vite": "^7.1.9",
    "wrangler": "^4.42.2"
  }
}

Production Example

This skill is based on patterns from:


Troubleshooting

Problem: MCP Inspector shows "Connection refused"

Solution: Ensure dev server is running on correct port and /mcp endpoint is configured:

npm run dev  # Should show port (usually 8787)
npx @modelcontextprotocol/inspector http://localhost:8787/mcp

Problem: Widget shows blank page in ChatGPT

Solution: Check browser console for errors. Common causes:

  1. CSP blocking external scripts - bundle them with Vite
  2. Missing window.openai check - widget loads before API ready
  3. CORS headers missing - add to /mcp/* route

Problem: Tool registered but never invoked

Solution: Improve tool description to be more action-oriented:

// ✅ Good
description: 'Use this when the user wants to see weather forecast for a specific city'

// ❌ Bad
description: 'Weather tool'

Complete Setup Checklist

  • Project scaffolded with npm create cloudflare
  • Dependencies installed (@modelcontextprotocol/sdk, hono, zod)
  • wrangler.jsonc configured with ASSETS binding
  • vite.config.ts configured with CloudFlare plugin
  • CORS middleware added to /mcp/* routes
  • At least one tool registered with MCP server
  • Tool has action-oriented description
  • Widget HTML created in src/widgets/
  • Resource registered with ui://widget/ URI pattern
  • MIME type set to text/html+skybridge
  • Tool handler returns _meta.initialData for widgets
  • Dev server runs without errors (npm run dev)
  • MCP Inspector connects successfully
  • Production build succeeds (npm run build)
  • Deployed to Cloudflare Workers (npx wrangler deploy)
  • Tested in ChatGPT developer mode

Questions? Issues?

  1. Check references/openai-metadata-format.md for all annotation fields
  2. Verify all steps in the setup process
  3. Check official MCP docs: https://modelcontextprotocol.io/
  4. Ensure CORS is configured to allow https://chatgpt.com
  5. Test with MCP Inspector before trying in ChatGPT

GitHub Repository

jezweb/claude-skills
Path: skills/openai-apps-mcp
aiautomationclaude-codeclaude-skillscloudflarereact

Related Skills

sglang

Meta

SGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.

View skill

evaluating-llms-harness

Testing

This Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.

View skill

llamaguard

Other

LlamaGuard is Meta's 7-8B parameter model for moderating LLM inputs and outputs across six safety categories like violence and hate speech. It offers 94-95% accuracy and can be deployed using vLLM, Hugging Face, or Amazon SageMaker. Use this skill to easily integrate content filtering and safety guardrails into your AI applications.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill