🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@openhands/extensions

Package Overview
Dependencies
Maintainers
3
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@openhands/extensions - npm Package Compare versions

Comparing version
0.5.0
to
0.6.0
+104
.github/workflows/pypi-publish.yml
name: Publish to PyPI
on:
workflow_dispatch:
release:
types: [published]
concurrency:
group: pypi-publish-${{ github.event.release.tag_name || github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
name: Build Python distributions
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Extract version
id: extract_version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${RELEASE_TAG#v}"
else
VERSION=$(python - <<'PY'
import tomllib
from pathlib import Path
print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"])
PY
)
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "📦 Version: $VERSION"
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
version: latest
python-version: '3.12'
- name: Validate package version matches release tag
if: github.event_name == 'release'
env:
VERSION: ${{ steps.extract_version.outputs.version }}
run: |
PACKAGE_VERSION=$(python - <<'PY'
import tomllib
from pathlib import Path
print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"])
PY
)
echo "Package version: $PACKAGE_VERSION"
echo "Release tag version: $VERSION"
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
echo "Error: pyproject.toml version ($PACKAGE_VERSION) doesn't match release tag ($VERSION)"
exit 1
fi
- name: Build package
run: uv build
- name: Upload distributions
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: dist/
if-no-files-found: error
publish:
name: Publish openhands-extensions to PyPI
if: >
github.event_name == 'workflow_dispatch' ||
!github.event.release.prerelease
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
name: pypi
url: https://pypi.org/p/openhands-extensions
permissions:
contents: read
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
// This file is auto-generated by scripts/build-integration-catalog.mjs.
// Do not edit it manually. To update it after changing integrations/catalog/*.json,
// run: npm run build:integrations
import entry0 from "./catalog/github.json" with { type: "json" };
import entry1 from "./catalog/slack.json" with { type: "json" };
import entry2 from "./catalog/tavily.json" with { type: "json" };
import entry3 from "./catalog/linear.json" with { type: "json" };
import entry4 from "./catalog/notion.json" with { type: "json" };
import entry5 from "./catalog/ordinal.json" with { type: "json" };
import entry6 from "./catalog/elevenlabs.json" with { type: "json" };
import entry7 from "./catalog/bitbucket.json" with { type: "json" };
import entry8 from "./catalog/gitlab.json" with { type: "json" };
import entry9 from "./catalog/xero.json" with { type: "json" };
import entry10 from "./catalog/quickbooks.json" with { type: "json" };
import entry11 from "./catalog/mailchimp.json" with { type: "json" };
import entry12 from "./catalog/pipedrive.json" with { type: "json" };
import entry13 from "./catalog/freshdesk.json" with { type: "json" };
import entry14 from "./catalog/servicenow.json" with { type: "json" };
import entry15 from "./catalog/okta.json" with { type: "json" };
import entry16 from "./catalog/plaid.json" with { type: "json" };
import entry17 from "./catalog/netlify.json" with { type: "json" };
import entry18 from "./catalog/vercel.json" with { type: "json" };
import entry19 from "./catalog/supabase.json" with { type: "json" };
import entry20 from "./catalog/posthog.json" with { type: "json" };
import entry21 from "./catalog/sentry.json" with { type: "json" };
import entry22 from "./catalog/datadog.json" with { type: "json" };
import entry23 from "./catalog/canva.json" with { type: "json" };
import entry24 from "./catalog/miro.json" with { type: "json" };
import entry25 from "./catalog/webflow.json" with { type: "json" };
import entry26 from "./catalog/zoom.json" with { type: "json" };
import entry27 from "./catalog/discord.json" with { type: "json" };
import entry28 from "./catalog/shopify.json" with { type: "json" };
import entry29 from "./catalog/stripe.json" with { type: "json" };
import entry30 from "./catalog/intercom.json" with { type: "json" };
import entry31 from "./catalog/zendesk.json" with { type: "json" };
import entry32 from "./catalog/hubspot.json" with { type: "json" };
import entry33 from "./catalog/salesforce.json" with { type: "json" };
import entry34 from "./catalog/sharepoint.json" with { type: "json" };
import entry35 from "./catalog/onedrive.json" with { type: "json" };
import entry36 from "./catalog/microsoft-teams.json" with { type: "json" };
import entry37 from "./catalog/microsoft-outlook.json" with { type: "json" };
import entry38 from "./catalog/box.json" with { type: "json" };
import entry39 from "./catalog/dropbox.json" with { type: "json" };
import entry40 from "./catalog/airtable.json" with { type: "json" };
import entry41 from "./catalog/monday.json" with { type: "json" };
import entry42 from "./catalog/clickup.json" with { type: "json" };
import entry43 from "./catalog/trello.json" with { type: "json" };
import entry44 from "./catalog/asana.json" with { type: "json" };
import entry45 from "./catalog/confluence.json" with { type: "json" };
import entry46 from "./catalog/jira.json" with { type: "json" };
import entry47 from "./catalog/google-calendar.json" with { type: "json" };
import entry48 from "./catalog/gmail.json" with { type: "json" };
import entry49 from "./catalog/google-sheets.json" with { type: "json" };
import entry50 from "./catalog/google-drive.json" with { type: "json" };
import entry51 from "./catalog/figma.json" with { type: "json" };
import entry52 from "./catalog/google-docs.json" with { type: "json" };
import entry53 from "./catalog/apify.json" with { type: "json" };
import entry54 from "./catalog/atlassian.json" with { type: "json" };
import entry55 from "./catalog/brave-search.json" with { type: "json" };
import entry56 from "./catalog/browser-mcp.json" with { type: "json" };
import entry57 from "./catalog/clickhouse.json" with { type: "json" };
import entry58 from "./catalog/cloudflare-bindings.json" with { type: "json" };
import entry59 from "./catalog/cloudflare-browser-rendering.json" with { type: "json" };
import entry60 from "./catalog/cloudflare-builds.json" with { type: "json" };
import entry61 from "./catalog/cloudflare-docs.json" with { type: "json" };
import entry62 from "./catalog/cloudflare-observability.json" with { type: "json" };
import entry63 from "./catalog/deepwiki.json" with { type: "json" };
import entry64 from "./catalog/everything.json" with { type: "json" };
import entry65 from "./catalog/exa.json" with { type: "json" };
import entry66 from "./catalog/fetch.json" with { type: "json" };
import entry67 from "./catalog/filesystem.json" with { type: "json" };
import entry68 from "./catalog/firecrawl.json" with { type: "json" };
import entry69 from "./catalog/git.json" with { type: "json" };
import entry70 from "./catalog/huggingface.json" with { type: "json" };
import entry71 from "./catalog/kagi.json" with { type: "json" };
import entry72 from "./catalog/memory.json" with { type: "json" };
import entry73 from "./catalog/mongodb.json" with { type: "json" };
import entry74 from "./catalog/neon.json" with { type: "json" };
import entry75 from "./catalog/obsidian.json" with { type: "json" };
import entry76 from "./catalog/paypal.json" with { type: "json" };
import entry77 from "./catalog/playwright.json" with { type: "json" };
import entry78 from "./catalog/redis.json" with { type: "json" };
import entry79 from "./catalog/resend.json" with { type: "json" };
import entry80 from "./catalog/sequential-thinking.json" with { type: "json" };
import entry81 from "./catalog/time.json" with { type: "json" };
export const INTEGRATION_CATALOG_ENTRIES = [
entry0,
entry1,
entry2,
entry3,
entry4,
entry5,
entry6,
entry7,
entry8,
entry9,
entry10,
entry11,
entry12,
entry13,
entry14,
entry15,
entry16,
entry17,
entry18,
entry19,
entry20,
entry21,
entry22,
entry23,
entry24,
entry25,
entry26,
entry27,
entry28,
entry29,
entry30,
entry31,
entry32,
entry33,
entry34,
entry35,
entry36,
entry37,
entry38,
entry39,
entry40,
entry41,
entry42,
entry43,
entry44,
entry45,
entry46,
entry47,
entry48,
entry49,
entry50,
entry51,
entry52,
entry53,
entry54,
entry55,
entry56,
entry57,
entry58,
entry59,
entry60,
entry61,
entry62,
entry63,
entry64,
entry65,
entry66,
entry67,
entry68,
entry69,
entry70,
entry71,
entry72,
entry73,
entry74,
entry75,
entry76,
entry77,
entry78,
entry79,
entry80,
entry81,
];
{
"id": "asana",
"name": "Asana",
"description": "Task management, projects, and work tracking.",
"categories": [
"Project management",
"Operations"
],
"appUrl": "https://asana.com",
"docsUrl": "https://developers.asana.com/docs/oauth",
"notes": "Broad business adoption and straightforward OAuth app model.",
"popularityRank": 13,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://app.asana.com/-/oauth_authorize",
"tokenUrl": "https://app.asana.com/-/oauth_token",
"scopes": [
"tasks:read"
]
}
},
"http": {
"apiBaseUrl": "https://app.asana.com/api/1.0",
"defaultTool": {
"name": "list_tasks",
"description": "List tasks visible to the connected Asana user.",
"method": "GET",
"path": "/tasks"
}
}
}
]
}
{
"id": "bitbucket",
"name": "Bitbucket",
"description": "Repositories, pull requests, and Atlassian engineering workflows.",
"categories": [
"Engineering",
"Source control"
],
"appUrl": "https://bitbucket.org",
"docsUrl": "https://developer.atlassian.com/cloud/bitbucket/oauth-2/",
"notes": "Popular in Atlassian-centric teams and complements Jira/Confluence.",
"popularityRank": 50,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://bitbucket.org/site/oauth2/authorize",
"tokenUrl": "https://bitbucket.org/site/oauth2/access_token",
"scopes": [
"account",
"repository"
]
}
},
"http": {
"apiBaseUrl": "https://api.bitbucket.org/2.0",
"defaultTool": {
"name": "get_current_user",
"description": "Fetch the authenticated Bitbucket user profile.",
"method": "GET",
"path": "/user"
}
}
}
]
}
{
"id": "box",
"name": "Box",
"description": "Enterprise file storage, metadata, and collaboration.",
"categories": [
"Storage",
"Enterprise"
],
"appUrl": "https://www.box.com",
"docsUrl": "https://developer.box.com/guides/authentication/oauth2/",
"notes": "Strong enterprise document-management footprint.",
"popularityRank": 19,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://account.box.com/api/oauth2/authorize",
"tokenUrl": "https://api.box.com/oauth2/token",
"scopes": [
"root_readonly",
"item_read"
]
}
},
"http": {
"apiBaseUrl": "https://api.box.com/2.0",
"defaultTool": {
"name": "list_root_items",
"description": "List files and folders in the Box root folder.",
"method": "GET",
"path": "/folders/0/items"
}
}
}
]
}
{
"id": "canva",
"name": "Canva",
"description": "Design content, brand assets, and marketing collateral workflows.",
"categories": [
"Design",
"Marketing"
],
"appUrl": "https://www.canva.com",
"docsUrl": "https://www.canva.dev/docs/connect/oauth/",
"notes": "Broad creator and marketing adoption with OAuth-based apps.",
"popularityRank": 34,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"tokenUrl": "https://api.canva.com/rest/v1/oauth/token",
"scopes": []
}
},
"http": {
"apiBaseUrl": "https://api.canva.com/rest/v1",
"defaultTool": {
"name": "list_designs",
"description": "List Canva designs available to the connected user.",
"method": "GET",
"path": "/designs"
}
}
}
]
}
{
"id": "clickup",
"name": "ClickUp",
"description": "Tasks, docs, goals, and workflow automation.",
"categories": [
"Project management",
"Operations"
],
"appUrl": "https://clickup.com",
"docsUrl": "https://clickup.com/api/developer-portal/authentication",
"notes": "Large install base and broad operations coverage.",
"popularityRank": 15,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://app.clickup.com/api",
"tokenUrl": "https://api.clickup.com/api/v2/oauth/token",
"scopes": []
}
},
"http": {
"apiBaseUrl": "https://api.clickup.com/api/v2",
"defaultTool": {
"name": "list_workspaces",
"description": "List ClickUp workspaces available to the connected user.",
"method": "GET",
"path": "/team"
}
}
}
]
}
{
"id": "confluence",
"name": "Confluence",
"description": "Wiki pages, spaces, and internal documentation search.",
"categories": [
"Documentation",
"Knowledge base"
],
"appUrl": "https://www.atlassian.com/software/confluence",
"docsUrl": "https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/",
"notes": "Natural companion to Jira for software teams.",
"popularityRank": 11,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://auth.atlassian.com/authorize",
"tokenUrl": "https://auth.atlassian.com/oauth/token",
"scopes": [
"read:page:confluence"
]
}
},
"http": {
"apiBaseUrl": "https://api.atlassian.com/ex/confluence/{cloudId}",
"defaultTool": {
"name": "list_spaces",
"description": "List Confluence spaces available to the connected user.",
"method": "GET",
"path": "/wiki/api/v2/spaces"
}
}
}
]
}
{
"id": "datadog",
"name": "Datadog",
"description": "Logs, metrics, monitors, incidents, and observability workflows.",
"categories": [
"Observability",
"Operations"
],
"appUrl": "https://www.datadoghq.com",
"docsUrl": "https://docs.datadoghq.com/bits_ai/mcp_server/setup/",
"notes": "Uses Datadog's official hosted MCP server. The default registration targets the US1 endpoint and can be edited for other Datadog sites.",
"popularityRank": 35,
"connectionOptions": [
{
"id": "oauth",
"provider": "mcp",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://app.datadoghq.com/oauth2/v1/authorize",
"tokenUrl": "https://api.datadoghq.com/oauth2/v1/token",
"scopes": [
"dashboards_read",
"monitors_read"
]
}
},
"transport": {
"kind": "shttp",
"url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp"
}
}
]
}
{
"id": "discord",
"name": "Discord",
"description": "Guilds, channels, messages, and community operations.",
"categories": [
"Communication",
"Community"
],
"appUrl": "https://discord.com",
"docsUrl": "https://discord.com/developers/docs/topics/oauth2",
"notes": "Useful for community automation and bot-assisted workflows.",
"popularityRank": 30,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://discord.com/oauth2/authorize",
"tokenUrl": "https://discord.com/api/oauth2/token",
"scopes": [
"identify",
"guilds"
]
}
},
"http": {
"apiBaseUrl": "https://discord.com/api/v10",
"defaultTool": {
"name": "list_guilds",
"description": "List Discord guilds available to the connected user.",
"method": "GET",
"path": "/users/@me/guilds"
}
}
}
]
}
{
"id": "dropbox",
"name": "Dropbox",
"description": "Cloud files, folders, content access, and sharing.",
"categories": [
"Storage",
"Documents"
],
"appUrl": "https://www.dropbox.com",
"docsUrl": "https://developers.dropbox.com/oauth-guide",
"notes": "Popular file automation target with mature OAuth support.",
"popularityRank": 18,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://www.dropbox.com/oauth2/authorize",
"tokenUrl": "https://api.dropboxapi.com/oauth2/token",
"scopes": [
"files.metadata.read"
]
}
},
"http": {
"apiBaseUrl": "https://api.dropboxapi.com/2",
"defaultTool": {
"name": "list_root_folder",
"description": "List entries in the root Dropbox folder.",
"method": "POST",
"path": "/files/list_folder"
}
}
}
]
}
{
"id": "freshdesk",
"name": "Freshdesk",
"description": "Support tickets, customer records, and help desk workflows.",
"categories": [
"Support",
"Operations"
],
"appUrl": "https://www.freshworks.com/freshdesk/",
"docsUrl": "https://developers.freshdesk.com/api/#authentication",
"notes": "Popular support platform for SMB and mid-market teams.",
"popularityRank": 44,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://{domain}.freshdesk.com/oauth/authorize",
"tokenUrl": "https://{domain}.freshdesk.com/oauth/token",
"scopes": [
"tickets_view"
]
}
},
"http": {
"apiBaseUrl": "https://{domain}.freshdesk.com/api/v2",
"defaultTool": {
"name": "list_tickets",
"description": "List Freshdesk tickets for the connected account.",
"method": "GET",
"path": "/tickets"
}
}
}
]
}
{
"id": "gitlab",
"name": "GitLab",
"description": "Repositories, issues, merge requests, pipelines, and DevSecOps.",
"categories": [
"Engineering",
"Source control"
],
"appUrl": "https://about.gitlab.com",
"docsUrl": "https://docs.gitlab.com/integration/oauth_provider/",
"notes": "Major Git hosting platform and natural expansion beyond GitHub.",
"popularityRank": 49,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://gitlab.com/oauth/authorize",
"tokenUrl": "https://gitlab.com/oauth/token",
"scopes": [
"read_api"
]
}
},
"http": {
"apiBaseUrl": "https://gitlab.com/api/v4",
"defaultTool": {
"name": "get_current_user",
"description": "Fetch the authenticated GitLab user profile.",
"method": "GET",
"path": "/user"
}
}
}
]
}
{
"id": "gmail",
"name": "Gmail",
"description": "Mailbox search, drafting, sending, and thread triage.",
"categories": [
"Communication",
"Email"
],
"appUrl": "https://workspace.google.com/products/gmail/",
"docsUrl": "https://developers.google.com/identity/protocols/oauth2",
"notes": "Popular agent workflow target for inbox triage and drafting.",
"popularityRank": 8,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly"
]
}
},
"http": {
"apiBaseUrl": "https://gmail.googleapis.com",
"defaultTool": {
"name": "list_messages",
"description": "List Gmail messages for the authenticated user.",
"method": "GET",
"path": "/gmail/v1/users/me/messages"
}
}
}
]
}
{
"id": "google-calendar",
"name": "Google Calendar",
"description": "Calendar search, event scheduling, and meeting coordination.",
"categories": [
"Calendar",
"Scheduling"
],
"appUrl": "https://workspace.google.com/products/calendar/",
"docsUrl": "https://developers.google.com/identity/protocols/oauth2",
"notes": "Strong personal productivity use case with mature OAuth flows.",
"popularityRank": 9,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/calendar.readonly"
]
}
},
"http": {
"apiBaseUrl": "https://www.googleapis.com/calendar/v3",
"defaultTool": {
"name": "list_events",
"description": "List events from the user's primary Google Calendar.",
"method": "GET",
"path": "/calendars/primary/events"
}
}
}
]
}
{
"id": "google-docs",
"name": "Google Docs",
"description": "Docs authoring and Google Workspace document automation.",
"categories": [
"Documents",
"Knowledge base"
],
"appUrl": "https://workspace.google.com/products/docs/",
"docsUrl": "https://developers.google.com/identity/protocols/oauth2",
"notes": "Current managed connector accepts a Google access token manually; a full OAuth connect flow can remove token copy/paste.",
"popularityRank": 2,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/documents.readonly"
]
}
},
"http": {
"apiBaseUrl": "https://docs.googleapis.com",
"defaultTool": {
"name": "get_document",
"description": "Fetch a Google Docs document by ID.",
"method": "GET",
"path": "/v1/documents/{documentId}"
}
}
}
]
}
{
"id": "google-drive",
"name": "Google Drive",
"description": "File search, metadata, folders, and document discovery.",
"categories": [
"Storage",
"Documents"
],
"appUrl": "https://workspace.google.com/products/drive/",
"docsUrl": "https://developers.google.com/identity/protocols/oauth2",
"notes": "Natural expansion of the Google Workspace surface beyond Google Docs.",
"popularityRank": 6,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
}
},
"http": {
"apiBaseUrl": "https://www.googleapis.com/drive/v3",
"defaultTool": {
"name": "list_files",
"description": "List Drive files visible to the connected user.",
"method": "GET",
"path": "/files"
}
}
}
]
}
{
"id": "google-sheets",
"name": "Google Sheets",
"description": "Spreadsheet reads, writes, formulas, and reporting workflows.",
"categories": [
"Spreadsheets",
"Analytics"
],
"appUrl": "https://workspace.google.com/products/sheets/",
"docsUrl": "https://developers.google.com/identity/protocols/oauth2",
"notes": "High-value automation surface for agent-driven reporting and structured edits.",
"popularityRank": 7,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets.readonly"
]
}
},
"http": {
"apiBaseUrl": "https://sheets.googleapis.com",
"defaultTool": {
"name": "get_spreadsheet",
"description": "Fetch spreadsheet metadata and sheets by ID.",
"method": "GET",
"path": "/v4/spreadsheets/{spreadsheetId}"
}
}
}
]
}
{
"id": "hubspot",
"name": "HubSpot",
"description": "CRM, marketing, tickets, and customer lifecycle workflows.",
"categories": [
"CRM",
"Marketing"
],
"appUrl": "https://www.hubspot.com",
"docsUrl": "https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server",
"notes": "Uses HubSpot's official remote MCP server plus MCP auth apps with PKCE.",
"popularityRank": 25,
"connectionOptions": [
{
"id": "oauth",
"provider": "mcp",
"auth": {
"strategy": "oauth2",
"credentialHelp": "Use the client ID and secret from a HubSpot MCP auth app (Development → MCP Auth Apps). Standard HubSpot OAuth apps and private apps will not authenticate with mcp.hubspot.com.",
"oauth": {
"authorizationUrl": "https://mcp.hubspot.com/oauth/authorize/user",
"tokenUrl": "https://mcp.hubspot.com/oauth/v3/token",
"scopes": [],
"pkce": true,
"clientAuthentication": "body"
}
},
"transport": {
"kind": "shttp",
"url": "https://mcp.hubspot.com"
}
}
]
}
{
"id": "intercom",
"name": "Intercom",
"description": "Customer support, conversations, inboxes, and CRM context.",
"categories": [
"Support",
"CRM"
],
"appUrl": "https://www.intercom.com",
"docsUrl": "https://developers.intercom.com/building-apps/docs/authentication-types",
"notes": "Useful for customer-facing assistant workflows.",
"popularityRank": 27,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://app.intercom.com/oauth",
"tokenUrl": "https://api.intercom.io/auth/eagle/token",
"scopes": [
"read_users",
"read_conversations"
]
}
},
"http": {
"apiBaseUrl": "https://api.intercom.io",
"defaultTool": {
"name": "list_contacts",
"description": "List Intercom contacts for the connected workspace.",
"method": "GET",
"path": "/contacts"
}
}
}
]
}
{
"id": "jira",
"name": "Jira",
"description": "Issue tracking, sprint workflows, and engineering program management.",
"categories": [
"Project management",
"Engineering"
],
"appUrl": "https://www.atlassian.com/software/jira",
"docsUrl": "https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/",
"notes": "Very common MCP target for software teams and ticket automation.",
"popularityRank": 10,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://auth.atlassian.com/authorize",
"tokenUrl": "https://auth.atlassian.com/oauth/token",
"scopes": [
"read:jira-work"
]
}
},
"http": {
"apiBaseUrl": "https://api.atlassian.com/ex/jira/{cloudId}",
"defaultTool": {
"name": "list_projects",
"description": "List Jira projects available to the connected user.",
"method": "GET",
"path": "/rest/api/3/project/search"
}
}
}
]
}
{
"id": "mailchimp",
"name": "Mailchimp",
"description": "Campaigns, audiences, automations, and email marketing.",
"categories": [
"Marketing",
"Email"
],
"appUrl": "https://mailchimp.com",
"docsUrl": "https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/",
"notes": "Popular marketing automation target.",
"popularityRank": 46,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.mailchimp.com/oauth2/authorize",
"tokenUrl": "https://login.mailchimp.com/oauth2/token",
"scopes": [
"audiences:read"
]
}
},
"http": {
"apiBaseUrl": "https://{dc}.api.mailchimp.com/3.0",
"defaultTool": {
"name": "list_audiences",
"description": "List Mailchimp audiences for the connected account.",
"method": "GET",
"path": "/lists"
}
}
}
]
}
{
"id": "microsoft-outlook",
"name": "Microsoft Outlook",
"description": "Mail, calendar, contacts, and meeting workflows through Microsoft Graph.",
"categories": [
"Email",
"Calendar"
],
"appUrl": "https://www.microsoft.com/microsoft-365/outlook/email-and-calendar-software-microsoft-outlook",
"docsUrl": "https://learn.microsoft.com/graph/auth-v2-user",
"notes": "High-value Microsoft Graph integration surface for enterprise users.",
"popularityRank": 20,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scopes": [
"Mail.Read"
]
}
},
"http": {
"apiBaseUrl": "https://graph.microsoft.com/v1.0",
"defaultTool": {
"name": "list_messages",
"description": "List Outlook messages for the signed-in user.",
"method": "GET",
"path": "/me/messages"
}
}
}
]
}
{
"id": "microsoft-teams",
"name": "Microsoft Teams",
"description": "Chats, channels, meetings, and collaboration via Microsoft Graph.",
"categories": [
"Communication",
"Enterprise"
],
"appUrl": "https://www.microsoft.com/microsoft-teams/group-chat-software",
"docsUrl": "https://learn.microsoft.com/graph/auth-v2-user",
"notes": "Common enterprise chat target alongside Outlook.",
"popularityRank": 21,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scopes": [
"Team.ReadBasic.All"
]
}
},
"http": {
"apiBaseUrl": "https://graph.microsoft.com/v1.0",
"defaultTool": {
"name": "list_teams",
"description": "List Microsoft Teams joined by the signed-in user.",
"method": "GET",
"path": "/me/joinedTeams"
}
}
}
]
}
{
"id": "miro",
"name": "Miro",
"description": "Whiteboards, diagrams, notes, and workshop collaboration.",
"categories": [
"Whiteboard",
"Collaboration"
],
"appUrl": "https://miro.com",
"docsUrl": "https://developers.miro.com/docs/rest-api-build-your-first-oauth-app",
"notes": "Strong visual collaboration target for design and product teams.",
"popularityRank": 33,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://miro.com/oauth/authorize",
"tokenUrl": "https://api.miro.com/v1/oauth/token",
"scopes": [
"boards:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.miro.com/v2",
"defaultTool": {
"name": "list_boards",
"description": "List Miro boards available to the connected user.",
"method": "GET",
"path": "/boards"
}
}
}
]
}
{
"id": "monday",
"name": "Monday.com",
"description": "Boards, automations, and work management across teams.",
"categories": [
"Project management",
"Operations"
],
"appUrl": "https://monday.com",
"docsUrl": "https://developer.monday.com/apps/docs/oauth",
"notes": "High-demand operations platform with rich board APIs.",
"popularityRank": 16,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://auth.monday.com/oauth2/authorize",
"tokenUrl": "https://auth.monday.com/oauth2/token",
"scopes": [
"boards:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.monday.com/v2",
"defaultTool": {
"name": "list_boards",
"description": "Query boards from Monday.com.",
"method": "POST",
"path": "/"
}
}
}
]
}
{
"id": "netlify",
"name": "Netlify",
"description": "Sites, builds, deploy previews, and web operations.",
"categories": [
"Deployment",
"Developer tools"
],
"appUrl": "https://www.netlify.com",
"docsUrl": "https://docs.netlify.com/api/get-started/#oauth-applications",
"notes": "Common alternative hosting/deployment automation target.",
"popularityRank": 40,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://app.netlify.com/authorize",
"tokenUrl": "https://api.netlify.com/oauth/token",
"scopes": [
"sites:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.netlify.com/api/v1",
"defaultTool": {
"name": "list_sites",
"description": "List Netlify sites available to the connected user.",
"method": "GET",
"path": "/sites"
}
}
}
]
}
{
"id": "okta",
"name": "Okta",
"description": "Identity, users, applications, and access administration.",
"categories": [
"Identity",
"Enterprise"
],
"appUrl": "https://www.okta.com",
"docsUrl": "https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/",
"notes": "High-value admin and security automation surface.",
"popularityRank": 42,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://{yourOktaDomain}/oauth2/v1/authorize",
"tokenUrl": "https://{yourOktaDomain}/oauth2/v1/token",
"scopes": [
"okta.users.read"
]
}
},
"http": {
"apiBaseUrl": "https://{yourOktaDomain}/api/v1",
"defaultTool": {
"name": "list_users",
"description": "List Okta users for the connected org.",
"method": "GET",
"path": "/users"
}
}
}
]
}
{
"id": "onedrive",
"name": "OneDrive",
"description": "Cloud file access and document workflows through Microsoft Graph.",
"categories": [
"Storage",
"Documents"
],
"appUrl": "https://www.microsoft.com/microsoft-365/onedrive/online-cloud-storage",
"docsUrl": "https://learn.microsoft.com/graph/auth-v2-user",
"notes": "Useful for enterprise file automation and search.",
"popularityRank": 22,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scopes": [
"Files.Read"
]
}
},
"http": {
"apiBaseUrl": "https://graph.microsoft.com/v1.0",
"defaultTool": {
"name": "list_drive_items",
"description": "List OneDrive items in the root folder.",
"method": "GET",
"path": "/me/drive/root/children"
}
}
}
]
}
{
"id": "ordinal",
"name": "Ordinal",
"description": "Social media posts, profiles, analytics, labels, approvals, and engagements.",
"categories": [
"Social media",
"Marketing"
],
"appUrl": "https://app.tryordinal.com",
"docsUrl": "https://docs.tryordinal.com/api/mcp",
"logoUrl": "https://app.tryordinal.com/favicon.ico",
"notes": "Official MCP server backed by Ordinal's API with API-key (Bearer) auth.",
"popularityRank": 52,
"connectionOptions": [
{
"id": "api",
"provider": "mcp",
"auth": {
"strategy": "bearer",
"authModes": [
"bearer"
],
"credentialLabel": "Ordinal API key",
"credentialPlaceholder": "Paste your Ordinal API key",
"credentialHelp": "API key from your Ordinal workspace settings, sent as a Bearer token in the Authorization header."
},
"transport": {
"kind": "shttp",
"url": "https://app.tryordinal.com/api/mcp"
}
}
]
}
{
"id": "pipedrive",
"name": "Pipedrive",
"description": "Deals, contacts, and sales pipeline management.",
"categories": [
"CRM",
"Sales"
],
"appUrl": "https://www.pipedrive.com",
"docsUrl": "https://pipedrive.readme.io/docs/marketplace-oauth-authorization",
"notes": "Common CRM choice with solid OAuth story.",
"popularityRank": 45,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://oauth.pipedrive.com/oauth/authorize",
"tokenUrl": "https://oauth.pipedrive.com/oauth/token",
"scopes": [
"deals:read",
"contacts:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.pipedrive.com/v1",
"defaultTool": {
"name": "list_persons",
"description": "List Pipedrive people visible to the connected user.",
"method": "GET",
"path": "/persons"
}
}
}
]
}
{
"id": "plaid",
"name": "Plaid",
"description": "Financial account connectivity and transaction data flows.",
"categories": [
"Finance",
"Payments"
],
"appUrl": "https://plaid.com",
"docsUrl": "https://plaid.com/docs/auth/oauth/",
"notes": "Strong fintech workflow candidate, although much of Plaid's OAuth guidance is a special-case token exchange rather than a generic end-user SaaS OAuth connector.",
"popularityRank": 41,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2"
},
"http": {
"apiBaseUrl": "https://production.plaid.com",
"defaultTool": {
"name": "get_accounts",
"description": "Fetch accounts for a connected Plaid item.",
"method": "POST",
"path": "/accounts/get"
}
}
}
]
}
{
"id": "posthog",
"name": "PostHog",
"description": "Product analytics, feature flags, and session insights.",
"categories": [
"Analytics",
"Product"
],
"appUrl": "https://posthog.com",
"docsUrl": "https://posthog.com/docs/apps/build/oauth",
"notes": "Relevant for product analytics and experimentation workflows.",
"popularityRank": 37,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://us.posthog.com/oauth/authorize",
"tokenUrl": "https://us.posthog.com/oauth/token",
"scopes": [
"project:read"
]
}
},
"http": {
"apiBaseUrl": "https://us.posthog.com/api",
"defaultTool": {
"name": "list_projects",
"description": "List PostHog projects available to the connected user.",
"method": "GET",
"path": "/projects/"
}
}
}
]
}
{
"id": "quickbooks",
"name": "QuickBooks",
"description": "Accounting, invoices, customers, and financial operations.",
"categories": [
"Finance",
"Accounting"
],
"appUrl": "https://quickbooks.intuit.com",
"docsUrl": "https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0",
"notes": "Frequent finance automation request with OAuth-based apps.",
"popularityRank": 47,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://appcenter.intuit.com/connect/oauth2",
"tokenUrl": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"scopes": [
"com.intuit.quickbooks.accounting"
]
}
},
"http": {
"apiBaseUrl": "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}",
"defaultTool": {
"name": "list_customers",
"description": "Query QuickBooks customers for the connected company.",
"method": "GET",
"path": "/query?query=SELECT%20*%20FROM%20Customer%20MAXRESULTS%2010"
}
}
}
]
}
{
"id": "salesforce",
"name": "Salesforce",
"description": "CRM records, accounts, opportunities, and sales operations.",
"categories": [
"CRM",
"Sales"
],
"appUrl": "https://www.salesforce.com",
"docsUrl": "https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm&type=5",
"notes": "Very common enterprise CRM and agent-assist target.",
"popularityRank": 24,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.salesforce.com/services/oauth2/authorize",
"tokenUrl": "https://login.salesforce.com/services/oauth2/token",
"scopes": [
"api"
]
}
},
"http": {
"apiBaseUrl": "https://{instance}.salesforce.com/services/data/v60.0",
"defaultTool": {
"name": "list_accounts",
"description": "List Salesforce accounts from the connected org.",
"method": "GET",
"path": "/sobjects/Account"
}
}
}
]
}
{
"id": "servicenow",
"name": "ServiceNow",
"description": "Tickets, incidents, service catalogs, and enterprise workflows.",
"categories": [
"ITSM",
"Enterprise"
],
"appUrl": "https://www.servicenow.com",
"docsUrl": "https://www.servicenow.com/docs/bundle/xanadu-platform-security/page/administer/security/concept/oauth-concept.html",
"notes": "Strong enterprise IT operations use case.",
"popularityRank": 43,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://{instance}.service-now.com/oauth_auth.do",
"tokenUrl": "https://{instance}.service-now.com/oauth_token.do",
"scopes": [
"useraccount",
"table.read"
]
}
},
"http": {
"apiBaseUrl": "https://{instance}.service-now.com/api/now/v1",
"defaultTool": {
"name": "list_incidents",
"description": "List ServiceNow incidents for the connected instance.",
"method": "GET",
"path": "/table/incident"
}
}
}
]
}
{
"id": "sharepoint",
"name": "SharePoint",
"description": "Sites, document libraries, lists, and intranet content.",
"categories": [
"Knowledge base",
"Enterprise"
],
"appUrl": "https://www.microsoft.com/microsoft-365/sharepoint/collaboration",
"docsUrl": "https://learn.microsoft.com/graph/auth-v2-user",
"notes": "Frequently requested for enterprise knowledge retrieval.",
"popularityRank": 23,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scopes": [
"Sites.Read.All"
]
}
},
"http": {
"apiBaseUrl": "https://graph.microsoft.com/v1.0",
"defaultTool": {
"name": "get_root_site",
"description": "Fetch the SharePoint root site through Microsoft Graph.",
"method": "GET",
"path": "/sites/root"
}
}
}
]
}
{
"id": "shopify",
"name": "Shopify",
"description": "Storefronts, products, orders, and ecommerce operations.",
"categories": [
"Ecommerce",
"Operations"
],
"appUrl": "https://www.shopify.com",
"docsUrl": "https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant",
"notes": "A top ecommerce platform and strong MCP candidate.",
"popularityRank": 29,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://{shop}.myshopify.com/admin/oauth/authorize",
"tokenUrl": "https://{shop}.myshopify.com/admin/oauth/access_token",
"scopes": [
"read_products"
]
}
},
"http": {
"apiBaseUrl": "https://{shop}.myshopify.com/admin/api/2025-01",
"defaultTool": {
"name": "list_products",
"description": "List Shopify products for the connected store.",
"method": "GET",
"path": "/products.json"
}
}
}
]
}
{
"id": "trello",
"name": "Trello",
"description": "Boards, cards, checklists, and lightweight project planning.",
"categories": [
"Project management",
"Operations"
],
"appUrl": "https://trello.com",
"docsUrl": "https://developer.atlassian.com/cloud/trello/guides/rest-api/authorization/",
"notes": "Useful for SMB and cross-functional task boards, but Trello's public API auth is a special-case flow rather than a straightforward generic OAuth2 connector.",
"popularityRank": 14,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2"
},
"http": {
"apiBaseUrl": "https://api.trello.com/1",
"defaultTool": {
"name": "list_boards",
"description": "List Trello boards visible to the authenticated member.",
"method": "GET",
"path": "/members/me/boards"
}
}
}
]
}
{
"id": "vercel",
"name": "Vercel",
"description": "Deployments, projects, domains, and preview environment workflows.",
"categories": [
"Developer tools",
"Deployment"
],
"appUrl": "https://vercel.com",
"docsUrl": "https://vercel.com/docs/rest-api#oauth-apps",
"notes": "Especially relevant to this repo's deployment model.",
"popularityRank": 39,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://vercel.com/oauth/authorize",
"tokenUrl": "https://api.vercel.com/v2/oauth/access_token",
"scopes": [
"project:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.vercel.com",
"defaultTool": {
"name": "list_projects",
"description": "List Vercel projects available to the connected user.",
"method": "GET",
"path": "/v9/projects"
}
}
}
]
}
{
"id": "webflow",
"name": "Webflow",
"description": "CMS items, sites, and web publishing workflows.",
"categories": [
"CMS",
"Marketing"
],
"appUrl": "https://webflow.com",
"docsUrl": "https://developers.webflow.com/mcp/reference/getting-started",
"notes": "Uses Webflow's official hosted MCP server and deployment-scoped MCP OAuth registration so each user can authorize their own sites and workspaces.",
"popularityRank": 32,
"connectionOptions": [
{
"id": "oauth",
"provider": "mcp",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://mcp.webflow.com/oauth/authorize",
"tokenUrl": "https://mcp.webflow.com/oauth/token",
"scopes": [],
"pkce": true,
"clientAuthentication": "none",
"registrationUrl": "https://mcp.webflow.com/oauth/register"
}
},
"transport": {
"kind": "shttp",
"url": "https://mcp.webflow.com/mcp"
}
}
]
}
{
"id": "xero",
"name": "Xero",
"description": "Accounting, contacts, invoices, and bookkeeping workflows.",
"categories": [
"Finance",
"Accounting"
],
"appUrl": "https://www.xero.com",
"docsUrl": "https://developer.xero.com/documentation/guides/oauth2/overview/",
"notes": "Important accounting platform in many regions.",
"popularityRank": 48,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://login.xero.com/identity/connect/authorize",
"tokenUrl": "https://identity.xero.com/connect/token",
"scopes": [
"accounting.contacts.read"
]
}
},
"http": {
"apiBaseUrl": "https://api.xero.com/api.xro/2.0",
"defaultTool": {
"name": "list_contacts",
"description": "List Xero contacts for the connected tenant.",
"method": "GET",
"path": "/Contacts"
}
}
}
]
}
{
"id": "zendesk",
"name": "Zendesk",
"description": "Support tickets, customers, agents, and help desk operations.",
"categories": [
"Support",
"Operations"
],
"appUrl": "https://www.zendesk.com",
"docsUrl": "https://developer.zendesk.com/documentation/apps/getting-started/oauth/",
"notes": "Strong support automation use case for agents.",
"popularityRank": 26,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://{subdomain}.zendesk.com/oauth2/authorize",
"tokenUrl": "https://{subdomain}.zendesk.com/oauth2/token",
"scopes": [
"tickets:read"
]
}
},
"http": {
"apiBaseUrl": "https://{subdomain}.zendesk.com/api/v2",
"defaultTool": {
"name": "list_tickets",
"description": "List Zendesk tickets for the connected account.",
"method": "GET",
"path": "/tickets"
}
}
}
]
}
{
"id": "zoom",
"name": "Zoom",
"description": "Meetings, recordings, webinars, and scheduling operations.",
"categories": [
"Meetings",
"Calendar"
],
"appUrl": "https://zoom.us",
"docsUrl": "https://developers.zoom.us/docs/integrations/oauth/",
"notes": "Frequently requested for scheduling and meeting summaries.",
"popularityRank": 31,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://zoom.us/oauth/authorize",
"tokenUrl": "https://zoom.us/oauth/token",
"scopes": [
"meeting:read:user"
]
}
},
"http": {
"apiBaseUrl": "https://api.zoom.us/v2",
"defaultTool": {
"name": "list_meetings",
"description": "List Zoom meetings for the connected user.",
"method": "GET",
"path": "/users/me/meetings"
}
}
}
]
}
---
name: cobol-modernization
description: End-to-end COBOL to Java migration workflow. Handles build setup, mainframe dependency removal, and code migration with test validation.
license: MIT
compatibility: Requires GnuCOBOL, Java 11+, Maven/Gradle, Python 3.13 with uv
triggers:
- cobol modernization
- cobol to java
- cobol migration
- mainframe migration
---
End-to-end workflow for migrating COBOL codebases to Java.
## Overview
This plugin orchestrates a multi-phase COBOL modernization project:
1. **Build Setup** — Configure compilation for both COBOL and Java, create test fixtures
2. **Mainframe Planning** — Document transformations needed to remove mainframe dependencies
3. **Mainframe Removal** — Convert CICS/VSAM code to standard COBOL
4. **Java Migration** — Translate standardized COBOL to idiomatic Java
## Prerequisites
- GnuCOBOL compiler (`cobc`)
- Java 11+ with Maven or Gradle
- Python 3.13 with `uv`
- LLM API key (Anthropic or OpenAI)
## Quick Start
```bash
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
uv run python -m lc_sdk_examples.cobol_modernization --src-path /path/to/cobol/project
```
## Workflow Phases
### Phase 1: Build Setup
See [../build-setup/SKILL.md](../build-setup/SKILL.md)
Creates the foundation for the migration:
- COBOL compilation environment (GnuCOBOL)
- Java project structure (Maven/Gradle + JUnit 5)
- Test fixtures with golden outputs from COBOL execution
**Outputs:**
- `build_notes.md` — Build instructions
- `test-fixtures/` — Input/output test data
- `test_manifest.json` — Test case mapping
### Phase 2: Mainframe Planning
See [../mainframe-planning/SKILL.md](../mainframe-planning/SKILL.md)
Creates a transformation guide without modifying code:
- Maps CICS/VSAM constructs to standard COBOL equivalents
- Documents error handling replacements
- Identifies UI operations to stub
**Output:**
- `mainframe_dependency_removal_plan.md`
### Phase 3: Mainframe Removal
See [../mainframe-removal/SKILL.md](../mainframe-removal/SKILL.md)
Applies the transformation guide:
- Replaces EXEC CICS commands with file I/O
- Adds FILE STATUS checking
- Stubs BMS/screen operations
**Verification:**
- Code compiles with GnuCOBOL
- Runs with test fixtures
### Phase 4: Java Migration
See [../to-java-migration/SKILL.md](../to-java-migration/SKILL.md)
Translates to idiomatic Java:
- Proper Java conventions (not literal translations)
- JUnit tests using golden outputs
- COBOL references in comments
**Done when:**
- All code compiles
- All JUnit tests pass
- No TODOs or stubs remain
## Output Structure
```
your-project/
├── .lc-sdk/
│ ├── initial_batch_graph.json
│ ├── fixed_batch_graph.json
│ └── mainframe_dependency_removal_plan.md
├── test-fixtures/
│ ├── inputs/
│ └── expected_outputs/
├── test_manifest.json
├── src/main/java/
└── src/test/java/
```
## Troubleshooting
See [../../references/troubleshooting.md](../../references/troubleshooting.md) for common issues and solutions.
# CICS Transformation Examples
## UI/Terminal Operations
### Before (CICS)
```cobol
EXEC CICS SEND MAP('CUSTMAP') MAPSET('CUSTSET') END-EXEC
```
### After (Standard COBOL)
```cobol
DISPLAY "Customer Screen Output"
DISPLAY WS-CUSTOMER-NAME
DISPLAY WS-CUSTOMER-ADDRESS
```
---
## Error Handling
### Before (CICS with RESP/RESP2)
```cobol
EXEC CICS READ FILE('CUSTFILE')
INTO(WS-CUSTOMER-REC)
RIDFLD(WS-CUST-KEY)
RESP(WS-RESP)
RESP2(WS-RESP2)
END-EXEC
IF WS-RESP = DFHRESP(NOTFND)
PERFORM CUSTOMER-NOT-FOUND
END-IF
```
### After (Standard COBOL with FILE STATUS)
```cobol
READ CUSTFILE INTO WS-CUSTOMER-REC
KEY IS WS-CUST-KEY
IF FILE-STATUS NOT = '00'
IF FILE-STATUS = '23'
PERFORM CUSTOMER-NOT-FOUND
ELSE
PERFORM FILE-ERROR-HANDLER
END-IF
END-IF
```
---
name: mainframe-removal
description: Apply mainframe dependency transformations to COBOL code using a pre-generated transformation guide. Converts CICS/VSAM constructs to standard COBOL.
license: MIT
compatibility: Requires GnuCOBOL (cobc) for verification
triggers:
- remove mainframe
- cobol transformation
- cics removal
- standard cobol
---
Apply the transformations from a transformation guide to convert mainframe-specific COBOL to standard COBOL.
**Prerequisite**: A transformation guide must exist (see `cobol-mainframe-planning` skill).
## Transformation Requirements
### Data Operations
- Replace each CICS/VSAM construct with its standard COBOL equivalent per the plan
- Add FILE STATUS checks after EVERY file operation:
- Check for success (00) before proceeding
- Handle "not found" (23) distinctly from I/O errors (3x)
- Handle "file not exists" (35) at OPEN time
- Add explicit CLOSE statements in all code paths (including error paths)
### UI/Terminal Operations (BMS maps, SEND/RECEIVE)
- Replace with simple stubs or ACCEPT/DISPLAY statements
- Do NOT spend time replicating screen layouts
- Focus on preserving data flow, not UI fidelity
### Error Handling
- Replace CICS RESP/RESP2 checks with equivalent FILE STATUS logic
- Replace HANDLE CONDITION with explicit status checking after operations
- Ensure error paths don't leave files open
See [references/cics-transformation-examples.md](references/cics-transformation-examples.md) for before/after code examples.
## Verification
After transformation:
- Code MUST compile without errors
- Test with valid input → should execute core business logic
- Test with missing/invalid files → should fail gracefully, not crash
## Preserve
- All original business logic
- Data transformations and calculations
- Validation rules
## Checklist
- [ ] All EXEC CICS commands replaced
- [ ] FILE STATUS declared for all files
- [ ] FILE STATUS checked after every I/O operation
- [ ] CLOSE statements in all code paths
- [ ] Code compiles successfully
- [ ] Basic test execution passes
---
name: migration-scoring
description: Evaluate code migration quality with coverage, correctness, and style scoring. Generates executive reports with actionable recommendations.
license: MIT
compatibility: Requires completed migration with source and target code, Python 3.13 with uv
triggers:
- migration scoring
- migration quality
- migration evaluation
- score migration
---
Comprehensive quality evaluation for code migration projects.
## Overview
This plugin evaluates completed migrations through multiple lenses:
1. **Mapping** — Document source-to-target file relationships
2. **Quality Scoring** — Measure coverage and correctness
3. **Style Scoring** — Evaluate code quality and conventions
4. **Reporting** — Generate executive summary with recommendations
## Prerequisites
- Completed migration with both source and target code present
- Python 3.13 with `uv`
- LLM API key (Anthropic or OpenAI)
- Optional: Custom style rubric file
## Quick Start
```bash
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
uv run python -m lc_sdk_examples.migration_scoring \
--src-path /path/to/migration/project \
--rubric-path /path/to/style_rubric.txt
```
## Workflow Phases
### Phase 1: Migration Mapping
See [../migration-mapping/SKILL.md](../migration-mapping/SKILL.md)
Creates a source→target file mapping:
- Identifies which target files implement each source file
- Supports many-to-many relationships
- Flags unmigrated source files
**Output:** `migration_mapping.json`
```json
{
"CALC001.cbl": ["InvoiceCalculator.java", "TaxCalculator.java"],
"CUST002.cbl": ["CustomerService.java"]
}
```
### Phase 2: Quality Scoring
See [../score-quality/SKILL.md](../score-quality/SKILL.md)
Scores each source file on:
- **Coverage (1-5)**: How much functionality was migrated
- **Correctness (1-5)**: How accurately behavior was preserved
**Output:** `migration_score.json`
```json
{
"CALC001.cbl": {
"coverage": 4,
"correctness": 5,
"justification": "All calculation logic migrated..."
}
}
```
### Phase 3: Style Scoring
See [../score-style/SKILL.md](../score-style/SKILL.md)
Evaluates target code against style guidelines:
- Naming conventions
- Code organization
- Error handling
- Documentation
- Idiomaticity
**Output:** `style_score.json`
### Phase 4: Executive Report
See [../migration-report/SKILL.md](../migration-report/SKILL.md)
Generates a comprehensive report:
- Overall health assessment
- Score statistics and distribution
- Risk categorization (Green/Yellow/Red)
- Prioritized recommendations
**Output:** `final_report.md`
## Output Structure
```
your-project/
├── .lc-sdk/
│ ├── migration_mapping.json
│ ├── migration_score.json
│ ├── style_score.json
│ └── final_report.md
```
## Scoring Criteria
See [../score-quality/references/scoring-criteria.md](../score-quality/references/scoring-criteria.md) for the 1-5 scoring scales.
### Risk Categories
- **Green**: All scores ≥ 4
- **Yellow**: Any score 3-4
- **Red**: Any score < 3
"""Python bindings for the OpenHands extensions catalogs.
Mirrors the JavaScript package (``@openhands/extensions``): both read the same
hand-authored ``integrations/catalog/<id>.json`` files. The JavaScript package
uses a generated static import index; the Python package loads the packaged
individual JSON files directly. See ``integrations.py`` for the catalog API,
including filtering by connector type (mcp / oauth).
"""
from __future__ import annotations
from ._version import __version__
from .integrations import (
INTEGRATION_CATALOG_SNAPSHOT,
get_integration_catalog_entry,
list_integration_catalog,
)
__all__ = [
"INTEGRATION_CATALOG_SNAPSHOT",
"__version__",
"get_integration_catalog_entry",
"list_integration_catalog",
]
"""Package version, derived from installed package metadata.
The single source of truth is ``pyproject.toml``'s ``[project].version``
(which ``release-please`` bumps together with ``package.json`` via
``release-please-config.json`` -> ``extra-files``). At runtime we read the
installed distribution metadata via ``importlib.metadata`` so there is no
second hand-maintained version string. ``_FALLBACK_VERSION`` is only used
when the package is imported without being installed (e.g. straight off a
source checkout with no build), and ``tests/test_version_alignment.py``
asserts it stays in lock-step with ``pyproject.toml`` / ``package.json``.
"""
from __future__ import annotations
from importlib.metadata import PackageNotFoundError, version
#: Fallback used only when the package is not installed (no dist metadata).
#: Kept in lock-step with pyproject.toml/package.json by the version test.
_FALLBACK_VERSION = "0.6.0"
try:
__version__: str = version("openhands-extensions")
except PackageNotFoundError: # not installed (e.g. raw source checkout)
__version__ = _FALLBACK_VERSION
"""Python bindings for the OpenHands extensions integration catalog.
The source of truth is the hand-authored ``integrations/catalog/<id>.json``
directory. Wheels include those individual JSON files directly; no aggregate
catalog JSON is authored or packaged.
"""
from __future__ import annotations
import copy
import json
from functools import lru_cache
from importlib import resources
from pathlib import Path
from typing import Any, Iterable
__all__ = [
"INTEGRATION_CATALOG_SNAPSHOT",
"get_integration_catalog_entry",
"list_integration_catalog",
]
def _repo_catalog_dir() -> Path:
return Path(__file__).resolve().parents[2] / "integrations" / "catalog"
def _catalog_files() -> Iterable[Any]:
packaged = resources.files(__package__).joinpath("catalog")
if packaged.is_dir():
return sorted(
(path for path in packaged.iterdir() if path.name.endswith(".json")),
key=lambda path: path.name,
)
repo_catalog = _repo_catalog_dir()
return sorted(repo_catalog.glob("*.json"), key=lambda path: path.name)
def _read_json(path: Any) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
@lru_cache(maxsize=1)
def _integrations() -> tuple[dict[str, Any], ...]:
entries = [_read_json(path) for path in _catalog_files()]
entries.sort(
key=lambda entry: (-(entry.get("popularityRank") if entry.get("popularityRank") is not None else -1), entry["id"]),
)
return tuple(entries)
@lru_cache(maxsize=1)
def _integration_by_id() -> dict[str, dict[str, Any]]:
return {entry["id"]: entry for entry in _integrations()}
def _entry_supports_mcp(entry: dict[str, Any]) -> bool:
return any(option.get("provider") == "mcp" for option in entry.get("connectionOptions", []))
def _entry_supports_oauth(entry: dict[str, Any]) -> bool:
return any(
option.get("auth", {}).get("strategy") == "oauth2"
for option in entry.get("connectionOptions", [])
)
def list_integration_catalog(
mcp: bool | None = None,
oauth: bool | None = None,
) -> list[dict[str, Any]]:
"""Return the integration catalog, optionally filtered by connector type."""
result = []
for entry in _integrations():
if mcp is not None and _entry_supports_mcp(entry) != mcp:
continue
if oauth is not None and _entry_supports_oauth(entry) != oauth:
continue
result.append(copy.deepcopy(entry))
return result
def get_integration_catalog_entry(id: str) -> dict[str, Any] | None:
"""Return one integration catalog entry by id, or ``None``."""
entry = _integration_by_id().get(id)
return copy.deepcopy(entry) if entry is not None else None
INTEGRATION_CATALOG_SNAPSHOT: dict[str, Any] = {
"integrations": copy.deepcopy(list(_integrations()))
}
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const root = process.cwd();
const catalogDir = path.join(root, "integrations", "catalog");
const outputPath = path.join(root, "integrations", "catalog-index.js");
const files = (await readdir(catalogDir))
.filter((file) => file.endsWith(".json"))
.sort((left, right) => left.localeCompare(right));
const integrations = await Promise.all(
files.map(async (file) => ({
file,
entry: JSON.parse(await readFile(path.join(catalogDir, file), "utf8")),
})),
);
integrations.sort((left, right) => {
const rank = (right.entry.popularityRank ?? -1) - (left.entry.popularityRank ?? -1);
return rank || left.entry.id.localeCompare(right.entry.id);
});
const indexedIntegrations = integrations.map((integration, index) => ({
...integration,
importName: `entry${index}`,
}));
const imports = indexedIntegrations
.map(({ file, importName }) => `import ${importName} from "./catalog/${file}" with { type: "json" };`)
.join("\n");
const entries = indexedIntegrations.map(({ importName }) => ` ${importName},`).join("\n");
const header = `// This file is auto-generated by scripts/build-integration-catalog.mjs.
// Do not edit it manually. To update it after changing integrations/catalog/*.json,
// run: npm run build:integrations
`;
const body = `${header}${imports}
export const INTEGRATION_CATALOG_ENTRIES = [
${entries}
];
`;
await writeFile(outputPath, body);
{
"name": "agent-canvas-environment",
"version": "1.0.0",
"description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, frontend/backend port discovery, safe workspace hygiene, and local conversation delegation.",
"author": {
"name": "OpenHands",
"email": "contact@all-hands.dev"
},
"homepage": "https://github.com/OpenHands/extensions",
"repository": "https://github.com/OpenHands/extensions",
"license": "MIT",
"keywords": [
"agent-canvas",
"openhands",
"local",
"conversation",
"delegation"
]
}
# agent-canvas-environment
Guidance for working inside a local Agent Canvas environment, including local agent-server auth, safe workspace hygiene, and delegation to new local conversations.
See: [SKILL.md](./SKILL.md)
---
name: agent-canvas-environment
description: Work effectively inside a local Agent Canvas environment, including local agent-server auth, frontend/backend port discovery, safe workspace hygiene, and delegating work to a new local conversation through POST /api/conversations.
triggers:
- agent canvas
- agent-canvas
- local conversation
- delegate local conversation
- session api key
- X-Session-API-Key
- localhost:8001
---
# Agent Canvas Environment
Use this skill when running inside or alongside a local Agent Canvas stack, especially when the user asks to inspect the local backend, create or monitor local conversations, or delegate work to another local conversation.
## Core rules
- Treat the local Agent Canvas backend as an agent-server API, usually `http://localhost:8001`.
- Treat the local UI as a separate frontend, usually `http://localhost:8000`.
- Do not print session API keys. Pass them directly in `X-Session-API-Key`.
- Trust any runtime-services block or explicit user-provided host over default ports.
- Before mutating a repository, check `git status -sb`. If a worktree has unrelated changes, use a separate worktree or clone.
- When delegating, write a self-contained prompt. The new conversation does not inherit the current chat context.
## Find the session key
Use the first available value, without echoing it:
```bash
KEY="${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}"
if [ -z "$KEY" ] && [ -f "$HOME/.openhands/agent-canvas/api-key.txt" ]; then
KEY="$(tr -d '\n' < "$HOME/.openhands/agent-canvas/api-key.txt")"
fi
test -n "$KEY" || { echo "No Agent Canvas session API key found" >&2; exit 1; }
```
Validate backend access:
```bash
curl -sS -o /tmp/agent-canvas-conversations.json -w '%{http_code}\n' \
-H "X-Session-API-Key: $KEY" \
http://localhost:8001/api/conversations/search
```
HTTP `200` means the backend and key work.
## Delegate to a local conversation
Use `POST /api/conversations` with:
- current `/api/settings` as the base agent profile
- a fresh absolute workspace directory
- `initial_message.run: true`
- `worktree: false` when the workspace is already isolated
Template:
```bash
set -euo pipefail
BASE="${AGENT_CANVAS_BACKEND:-http://localhost:8001}"
KEY="${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}"
if [ -z "$KEY" ] && [ -f "$HOME/.openhands/agent-canvas/api-key.txt" ]; then
KEY="$(tr -d '\n' < "$HOME/.openhands/agent-canvas/api-key.txt")"
fi
test -n "$KEY" || { echo "No Agent Canvas session API key found" >&2; exit 1; }
WORKDIR="${WORKDIR:-$HOME/workspace/delegated/$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$WORKDIR"
SETTINGS_JSON="$(curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/settings")"
PROMPT='Write a complete, task-specific prompt here. Include repo, branch, constraints, validation, and expected report.'
PAYLOAD="$(jq -n --argjson settings "$SETTINGS_JSON" --arg prompt "$PROMPT" --arg workdir "$WORKDIR" '
def agent_settings:
($settings.agent_settings // {})
| del(.schema_version)
| . + {
agent_context: ((.agent_context // {}) + {
load_public_skills: true,
load_user_skills: true,
load_project_skills: true
})
};
($settings.conversation_settings // {}) as $conv |
{
agent_settings: agent_settings,
workspace: {kind: "LocalWorkspace", working_dir: $workdir},
confirmation_policy: {kind: "NeverConfirm"},
max_iterations: (($conv.max_iterations // 80) | if . == null then 80 else . end),
stuck_detection: true,
autotitle: true,
worktree: false,
initial_message: {
role: "user",
content: [{type: "text", text: $prompt}],
run: true
}
}
')"
curl -sS -X POST "$BASE/api/conversations" \
-H "Content-Type: application/json" \
-H "X-Session-API-Key: $KEY" \
--data-binary "$PAYLOAD" | jq '{id, title, execution_status, workspace}'
```
Report both links:
- UI: `http://localhost:8000/conversations/<conversation_id>`
- API: `http://localhost:8001/api/conversations/<conversation_id>`
## Monitor a delegated conversation
```bash
CID="<conversation_id>"
curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/conversations/$CID" \
| jq '{id, title, execution_status, updated_at, workspace, agent_kind: .agent.kind, current_model_id, current_model_name}'
curl -sS -H "X-Session-API-Key: $KEY" "$BASE/api/conversations/$CID/events/search?limit=20" \
| jq '.events // .items // .'
```
Terminal statuses commonly include `idle`, `running`, `finished`, `error`, `stuck`, and `stopped`.
## Prompt checklist for delegation
Include:
- repository owner/name and local path if relevant
- branch, PR, issue, or Linear ticket identifiers
- current status and known blockers
- exact files or subsystems in scope
- dirty-worktree warnings and paths not to touch
- whether to push, open a PR, or only report
- checks/tests to run
- expected final report format
Do not rely on the new conversation knowing anything from the current thread.
"""Assert integration catalog files drive the generated JS/Python assets."""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import openhands_extensions
ROOT = Path(__file__).resolve().parents[1]
CATALOG_DIR = ROOT / "integrations" / "catalog"
CATALOG_INDEX = ROOT / "integrations" / "catalog-index.js"
AGGREGATE_ASSETS = [
ROOT / "integrations" / "integration-catalog.json",
ROOT / "python" / "openhands_extensions" / "integration-catalog.json",
]
def _catalog_files() -> dict[str, dict]:
return {p.stem: json.loads(p.read_text()) for p in CATALOG_DIR.glob("*.json")}
def _catalog_entries() -> list[dict]:
entries = list(_catalog_files().values())
return sorted(entries, key=lambda entry: (-(entry.get("popularityRank") if entry.get("popularityRank") is not None else -1), entry["id"]))
def _supports_mcp(entry: dict) -> bool:
return any(option.get("provider") == "mcp" for option in entry["connectionOptions"])
def _supports_oauth(entry: dict) -> bool:
return any(option.get("auth", {}).get("strategy") == "oauth2" for option in entry["connectionOptions"])
def test_catalog_directory_is_hand_authored_source_of_truth() -> None:
for integration_id, entry in _catalog_files().items():
assert entry["id"] == integration_id
assert "supportsMcp" not in entry
assert "supportsOauth" not in entry
assert "oauthProvider" not in entry
assert "registrationDefaults" not in entry
assert "managedConnectorSlug" not in entry
assert "authStrategy" not in entry
assert "defaultConnectionOptionId" not in entry
assert "kind" not in entry
assert "catalogStatus" not in entry
assert "availability" not in entry
assert "runtimeAvailability" not in entry
def test_no_aggregate_catalog_json_exists() -> None:
for asset in AGGREGATE_ASSETS:
assert not asset.exists()
def test_generated_js_index_references_catalog_directory() -> None:
body = CATALOG_INDEX.read_text()
for file in sorted(path.name for path in CATALOG_DIR.glob("*.json")):
assert f"./catalog/{file}" in body
assert "integration-catalog.json" not in body
def test_python_snapshot_is_built_from_catalog_directory() -> None:
assert openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT == {
"integrations": _catalog_entries()
}
def test_python_list_integration_catalog_returns_raw_entries() -> None:
entries = openhands_extensions.list_integration_catalog()
assert entries == _catalog_entries()
for entry in entries:
assert "supportsMcp" not in entry
assert "supportsOauth" not in entry
assert "registrationDefaults" not in entry
assert "managedConnectorSlug" not in entry
assert "authStrategy" not in entry
assert "defaultConnectionOptionId" not in entry
assert "catalogStatus" not in entry
assert "availability" not in entry
assert "runtimeAvailability" not in entry
def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
entries = openhands_extensions.list_integration_catalog()
with_logo = [entry for entry in entries if entry.get("logoUrl")]
assert with_logo
assert any(entry["id"].startswith("cloudflare-") for entry in with_logo)
for entry in with_logo:
assert isinstance(entry["logoUrl"], str)
assert entry["logoUrl"].startswith("https://")
assert "react" not in entry["logoUrl"].lower()
def test_get_integration_catalog_entry_round_trip() -> None:
github = openhands_extensions.get_integration_catalog_entry("github")
assert github is not None
assert github == next(
entry for entry in openhands_extensions.list_integration_catalog() if entry["id"] == "github"
)
assert openhands_extensions.get_integration_catalog_entry("nope") is None
def _js_call(expr: str) -> str:
result = subprocess.run(
["node", "--input-type=module", "-e", expr],
capture_output=True,
text=True,
check=True,
cwd=ROOT,
)
return result.stdout
def test_js_reads_the_same_catalog_as_python() -> None:
integrations = _js_call(
"import { listIntegrationCatalog } from './integrations/index.js';\n"
"process.stdout.write(JSON.stringify(listIntegrationCatalog()));"
)
assert json.loads(integrations) == openhands_extensions.list_integration_catalog()
github = _js_call(
"import { getIntegrationCatalogEntry } from './integrations/index.js';\n"
"process.stdout.write(JSON.stringify(getIntegrationCatalogEntry('github')));"
)
assert json.loads(github) == openhands_extensions.get_integration_catalog_entry("github")
def _js_filter(mcp, oauth) -> list[str]:
expr = (
"import { listIntegrationCatalog } from './integrations/index.js';\n"
f"const f = listIntegrationCatalog({{ mcp: {mcp}, oauth: {oauth} }});\n"
"process.stdout.write(JSON.stringify(f.map((e) => e.id)));"
)
return json.loads(_js_call(expr))
def test_filter_mcp_only() -> None:
py_ids = {e["id"] for e in openhands_extensions.list_integration_catalog(mcp=True)}
js_ids = set(_js_filter("true", "undefined"))
all_mcp = {e["id"] for e in openhands_extensions.list_integration_catalog() if _supports_mcp(e)}
assert py_ids == js_ids == all_mcp
assert "filesystem" in py_ids
def test_filter_oauth_only() -> None:
py_ids = {e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True)}
js_ids = set(_js_filter("undefined", "true"))
all_oauth = {e["id"] for e in openhands_extensions.list_integration_catalog() if _supports_oauth(e)}
assert py_ids == js_ids == all_oauth
assert "github" in py_ids
def test_filter_oauth_not_mcp() -> None:
py_ids = {
e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True, mcp=False)
}
js_ids = set(_js_filter("false", "true"))
expected = {
e["id"]
for e in openhands_extensions.list_integration_catalog()
if _supports_oauth(e) and not _supports_mcp(e)
}
assert py_ids == js_ids == expected
def test_filter_none_returns_all() -> None:
assert len(openhands_extensions.list_integration_catalog()) == len(
openhands_extensions.list_integration_catalog(mcp=None, oauth=None)
)
def test_accessors_return_independent_copies() -> None:
catalog_a = openhands_extensions.list_integration_catalog()
catalog_b = openhands_extensions.list_integration_catalog()
assert catalog_a == catalog_b
assert catalog_a is not catalog_b
catalog_a[0]["__mutated"] = True
assert "__mutated" not in openhands_extensions.list_integration_catalog()[0]
github = openhands_extensions.get_integration_catalog_entry("github")
assert github is not None
github["__mutated"] = True
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry("github")
snapshot = openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT
snapshot["integrations"][0]["__mutated"] = True
assert "__mutated" not in openhands_extensions.list_integration_catalog()[0]
def test_hubspot_oauth_config_lives_on_connection_option() -> None:
hubspot = openhands_extensions.get_integration_catalog_entry("hubspot")
assert hubspot is not None
assert "registrationDefaults" not in hubspot
option = hubspot["connectionOptions"][0]
assert option["provider"] == "mcp"
assert option["transport"]["url"] == "https://mcp.hubspot.com"
oauth = option["auth"]["oauth"]
assert oauth["authorizationUrl"] == "https://mcp.hubspot.com/oauth/authorize/user"
assert oauth["tokenUrl"] == "https://mcp.hubspot.com/oauth/v3/token"
assert oauth["pkce"] is True
import os
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).parent.parent
/ "skills"
/ "slack-channel-monitor"
/ "scripts"
/ "main.py"
)
def load_slack_monitor_helpers():
os.environ["WORKSPACE_BASE"] = "/tmp/openhands/workspaces/slack-monitor-test/run"
source = SCRIPT_PATH.read_text(encoding="utf-8")
helper_source = source.split("\nPOLL_ITERATIONS = 10", 1)[0]
namespace: dict = {}
exec(compile(helper_source, str(SCRIPT_PATH), "exec"), namespace)
return namespace
def test_post_message_sends_markdown_text(monkeypatch):
helpers = load_slack_monitor_helpers()
posted: dict = {}
def fake_slack_post(token: str, endpoint: str, body: dict) -> dict:
posted["token"] = token
posted["endpoint"] = endpoint
posted["body"] = body
return {"ts": "123.456"}
monkeypatch.setitem(helpers, "slack_post", fake_slack_post)
markdown_summary = "✅ Done!\n\n- **Bold:** [link](https://example.com)"
ts = helpers["post_message"](
"xoxb-test",
"C123",
markdown_summary,
thread_ts="111.222",
)
assert ts == "123.456"
assert posted["endpoint"] == "chat.postMessage"
assert posted["body"] == {
"channel": "C123",
"markdown_text": markdown_summary,
"unfurl_links": False,
"unfurl_media": False,
"thread_ts": "111.222",
}
assert "text" not in posted["body"]
assert "blocks" not in posted["body"]
assert "mrkdwn" not in posted["body"]
def test_followup_quiet_poll_is_capped_at_watch_expiry(monkeypatch):
helpers = load_slack_monitor_helpers()
monkeypatch.setattr(helpers["time"], "time", lambda: 950.0)
calls: list[tuple[str, str, str]] = []
def fake_thread_replies(
token: str,
channel: str,
thread_ts: str,
oldest: str,
) -> list[dict]:
calls.append((channel, thread_ts, oldest))
return []
monkeypatch.setitem(helpers, "thread_replies", fake_thread_replies)
rec = {
"channel_id": "C123",
"thread_ts": "900.000000",
"status": "watching",
"last_seen_reply_ts": "900.000000",
"reply_poll_backoff_seconds": 80,
"next_reply_poll_at": 950.0,
"watch_until": 1000.0,
}
replies = helpers["_poll_due_thread_replies"](
"xoxb-test",
{"C123:900.000000": rec},
"UBOT",
[],
)
assert replies == []
assert calls == [("C123", "900.000000", "900.000000")]
assert rec["status"] == "watching"
assert rec["next_reply_poll_at"] == 1000.0
def test_expired_followup_watch_polls_once_before_closing(monkeypatch):
helpers = load_slack_monitor_helpers()
monkeypatch.setattr(helpers["time"], "time", lambda: 1001.0)
trigger = helpers["TRIGGER_PHRASE"]
reply = {
"ts": "999.500000",
"user": "U1",
"text": f"{trigger} please continue",
}
calls: list[tuple[str, str, str]] = []
def fake_thread_replies(
token: str,
channel: str,
thread_ts: str,
oldest: str,
) -> list[dict]:
calls.append((channel, thread_ts, oldest))
return [reply]
monkeypatch.setitem(helpers, "thread_replies", fake_thread_replies)
rec = {
"channel_id": "C123",
"thread_ts": "900.000000",
"status": "watching",
"last_seen_reply_ts": "900.000000",
"reply_poll_backoff_seconds": 160,
"next_reply_poll_at": 1100.0,
"watch_until": 1000.0,
}
replies = helpers["_poll_due_thread_replies"](
"xoxb-test",
{"C123:900.000000": rec},
"UBOT",
[],
)
assert calls == [("C123", "900.000000", "900.000000")]
assert replies == [("C123", reply)]
assert rec["last_seen_reply_ts"] == "999.500000"
assert (
rec["reply_poll_backoff_seconds"]
== helpers["THREAD_REPLY_INITIAL_BACKOFF_SECONDS"]
)
assert rec["next_reply_poll_at"] == 1006.0
assert rec["watch_until"] == 1301.0
"""Assert the JS (``package.json``) and Python (``pyproject.toml`` +
``_version.py``) package versions never drift apart.
``release-please`` is configured to bump both ``package.json`` and
``pyproject.toml`` together (see ``release-please-config.json`` ->
``extra-files``). This test is the guard that catches a manual edit that
bumps only one.
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _package_json_version() -> str:
text = (ROOT / "package.json").read_text()
match = re.search(r'"version"\s*:\s*"([^"]+)"', text)
assert match, "package.json has no version field"
return match.group(1)
def _pyproject_version() -> str:
text = (ROOT / "pyproject.toml").read_text()
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
assert match, "pyproject.toml has no version field"
return match.group(1)
def _version_module_version() -> str:
import openhands_extensions._version as v
text = (ROOT / "python" / "openhands_extensions" / "_version.py").read_text()
match = re.search(r'_FALLBACK_VERSION\s*=\s*"([^"]+)"', text)
assert match, "_version.py has no _FALLBACK_VERSION"
fallback = match.group(1)
# Runtime __version__ is metadata-derived when installed, fallback otherwise;
# both must agree with the declared versions.
assert v.__version__ in (fallback, _package_json_version()), (
f"runtime __version__ {v.__version__!r} disagrees with fallback/package.json"
)
return fallback
def test_package_json_and_pyproject_versions_match() -> None:
assert _package_json_version() == _pyproject_version(), (
"package.json and pyproject.toml versions differ. release-please "
"should bump both together; check release-please-config.json."
)
def test_version_module_matches_package_json() -> None:
assert _version_module_version() == _package_json_version(), (
"python/openhands_extensions/_version.py is out of sync with "
"package.json."
)
def test_installed_package_version_matches_source() -> None:
import openhands_extensions
assert openhands_extensions.__version__ == _package_json_version()
+6
-0
name: Tests
# NOTE: pull_request (not pull_request_target) so checks run against the PR head.
on:

@@ -22,2 +23,7 @@ pull_request:

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run tests

@@ -24,0 +30,0 @@ run: uv run --group test pytest tests/

+1
-1
{
".": "0.5.0"
".": "0.6.0"
}

@@ -93,3 +93,3 @@ # OpenHands Extensions — Agent Notes

- If you change top-level documentation, ensure links still resolve.
- `integrations/catalog/*.json` and `integrations/index.js` are the source of truth consumed by `@openhands/extensions`; agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links.
- `integrations/catalog/*.json` is the single hand-authored source of truth consumed by `@openhands/extensions`; adding or editing an integration should require changing exactly one JSON file in that directory. Do not reintroduce `integrations/integration-catalog.json`, separate provider files, per-language catalog duplicates, or provider-specific runtime code. Run `npm run build:integrations` after catalog edits to regenerate `integrations/catalog-index.js`, which statically imports each individual JSON file for the JS package. The Python package includes the same individual JSON files via wheel data and reads them directly. Agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links. JS exposes `listIntegrationCatalog({ mcp, oauth })` / `getIntegrationCatalogEntry`; Python mirrors with `list_integration_catalog(mcp=, oauth=)` / `get_integration_catalog_entry`. The legacy provider-catalog compatibility layer and `managedConnectorMigration` / `legacyScopeBundles` / `canonicalServerUrl` / `errorHints` mechanisms were removed intentionally; providers declare only standard OAuth config as integration data.
- For Python test runs, prefer `uv sync --group test` followed by `uv run pytest -q`; the full suite depends on `openhands-sdk`, which is not available in the base environment.

@@ -96,0 +96,0 @@ - Agent-driven plugins (for example `plugins/pr-review` and `plugins/release-notes`) use `uv run --with openhands-sdk --with openhands-tools ...` and require an `LLM_API_KEY` in addition to `GITHUB_TOKEN`.

@@ -5,15 +5,36 @@ {

"description": "List bases, query records, and update fields across your Airtable workspace.",
"categories": [
"Database",
"Operations"
],
"appUrl": "https://airtable.com",
"docsUrl": "https://github.com/domdomegg/airtable-mcp-server",
"iconBg": "#FCB400",
"iconColor": "var(--oh-surface-deep)",
"keywords": [
"spreadsheet",
"database",
"records",
"bases"
],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"notes": "Very common internal-tools and operations automation surface.",
"popularityRank": 17,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://airtable.com/oauth2/v1/authorize",
"tokenUrl": "https://airtable.com/oauth2/v1/token",
"scopes": [
"schema.bases:read",
"data.records:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.airtable.com/v0",
"defaultTool": {
"name": "list_bases",
"description": "List Airtable bases the connected user granted access to.",
"method": "GET",
"path": "/meta/bases"
}
}
},
{
"id": "api",

@@ -44,3 +65,12 @@ "provider": "mcp",

}
],
"iconBg": "#FCB400",
"logoUrl": "https://cdn.simpleicons.org/airtable/000000",
"iconColor": "var(--oh-surface-deep)",
"keywords": [
"spreadsheet",
"database",
"records",
"bases"
]
}

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#0052CC",
"logoUrl": "https://cdn.simpleicons.org/atlassian/FFFFFF",
"keywords": [

@@ -15,16 +16,12 @@ "jira",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://mcp.atlassian.com/v1/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://mcp.atlassian.com/v1/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -31,0 +28,0 @@ }

@@ -7,2 +7,3 @@ {

"iconBg": "#FB542B",
"logoUrl": "https://cdn.simpleicons.org/brave/FFFFFF",
"keywords": [

@@ -12,4 +13,2 @@ "search",

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -16,0 +15,0 @@ {

@@ -13,5 +13,2 @@ {

],
"kind": "mcp",
"runtimeAvailability": "local",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -18,0 +15,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#FFFF00",
"logoUrl": "https://cdn.simpleicons.org/clickhouse/000000",
"iconColor": "var(--oh-surface-deep)",

@@ -15,4 +16,2 @@ "keywords": [

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -19,0 +18,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#F38020",
"logoUrl": "https://cdn.simpleicons.org/cloudflare/FFFFFF",
"keywords": [

@@ -16,16 +17,12 @@ "cloudflare",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://bindings.mcp.cloudflare.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://bindings.mcp.cloudflare.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -32,0 +29,0 @@ }

@@ -7,2 +7,3 @@ {

"iconBg": "#F38020",
"logoUrl": "https://cdn.simpleicons.org/cloudflare/FFFFFF",
"keywords": [

@@ -14,16 +15,12 @@ "cloudflare",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://browser.mcp.cloudflare.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://browser.mcp.cloudflare.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -30,0 +27,0 @@ }

{
"id": "cloudflare-builds",
"name": "Cloudflare Builds",
"description": "Inspect Workers Builds — logs, statuses, and rerun failed deploys.",
"description": "Inspect Workers Builds \u2014 logs, statuses, and rerun failed deploys.",
"docsUrl": "https://developers.cloudflare.com/agents/model-context-protocol/",
"iconBg": "#F38020",
"logoUrl": "https://cdn.simpleicons.org/cloudflare/FFFFFF",
"keywords": [

@@ -14,16 +15,12 @@ "cloudflare",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://builds.mcp.cloudflare.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://builds.mcp.cloudflare.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -30,0 +27,0 @@ }

@@ -7,2 +7,3 @@ {

"iconBg": "#F38020",
"logoUrl": "https://cdn.simpleicons.org/cloudflare/FFFFFF",
"keywords": [

@@ -14,4 +15,2 @@ "cloudflare",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -22,9 +21,7 @@ {

"transport": {
"kind": "sse",
"url": "https://docs.mcp.cloudflare.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://docs.mcp.cloudflare.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "none"
}

@@ -31,0 +28,0 @@ }

@@ -7,2 +7,3 @@ {

"iconBg": "#F38020",
"logoUrl": "https://cdn.simpleicons.org/cloudflare/FFFFFF",
"keywords": [

@@ -15,16 +16,12 @@ "cloudflare",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://observability.mcp.cloudflare.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://observability.mcp.cloudflare.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -31,0 +28,0 @@ }

@@ -15,4 +15,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -23,9 +21,7 @@ {

"transport": {
"kind": "sse",
"url": "https://mcp.deepwiki.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://mcp.deepwiki.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "none"
}

@@ -32,0 +28,0 @@ }

@@ -5,39 +5,38 @@ {

"description": "Generate speech, clone voices, and transcribe audio via ElevenLabs.",
"categories": [
"Audio",
"AI"
],
"appUrl": "https://elevenlabs.io",
"docsUrl": "https://elevenlabs.io/docs/api-reference/mcp",
"iconBg": "var(--oh-color-base)",
"keywords": [
"tts",
"speech",
"voice",
"audio"
],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"notes": "Official API-key connector backed by ElevenLabs' published OpenAPI spec.",
"popularityRank": 51,
"connectionOptions": [
{
"id": "api",
"provider": "mcp",
"transport": {
"kind": "stdio",
"serverName": "elevenlabs",
"command": "uvx",
"args": [
"elevenlabs-mcp"
"provider": "http",
"auth": {
"strategy": "api_key",
"authModes": [
"api_key"
],
"envFields": [
{
"key": "ELEVENLABS_API_KEY",
"label": "ElevenLabs API key",
"type": "password",
"required": true,
"helperText": "API key from your ElevenLabs account settings.",
"helperLink": "https://elevenlabs.io/app/settings/api-keys"
}
]
"credentialLabel": "ElevenLabs API key",
"credentialPlaceholder": "Paste your ElevenLabs API key",
"credentialHelp": "Personal or workspace ElevenLabs API key sent in the xi-api-key header.",
"apiKeyHeaderName": "xi-api-key"
},
"auth": {
"strategy": "api_key"
"http": {
"apiBaseUrl": "https://api.elevenlabs.io",
"openApiUrl": "https://api.elevenlabs.io/openapi.json"
}
}
],
"iconBg": "var(--oh-color-base)",
"logoUrl": "https://cdn.simpleicons.org/elevenlabs/FFFFFF",
"keywords": [
"tts",
"speech",
"voice",
"audio"
]
}

@@ -12,4 +12,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -16,0 +14,0 @@ {

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -5,14 +5,38 @@ {

"description": "Read Figma frames, components, and styles to ground UI work in your designs.",
"categories": [
"Design",
"Frontend"
],
"appUrl": "https://www.figma.com",
"docsUrl": "https://github.com/GLips/Figma-Context-MCP",
"iconBg": "var(--oh-surface)",
"keywords": [
"design",
"ui",
"frames",
"components"
],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"notes": "",
"popularityRank": 5,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://www.figma.com/oauth",
"tokenUrl": "https://api.figma.com/v1/oauth/token",
"scopes": [
"current_user:read",
"file_content:read",
"file_metadata:read",
"projects:read"
]
}
},
"http": {
"apiBaseUrl": "https://api.figma.com",
"defaultTool": {
"name": "get_file",
"description": "Fetch a Figma file by key.",
"method": "GET",
"path": "/v1/files/{fileKey}"
}
}
},
{
"id": "api",

@@ -44,3 +68,11 @@ "provider": "mcp",

}
],
"iconBg": "var(--oh-surface)",
"logoUrl": "https://cdn.simpleicons.org/figma/FFFFFF",
"keywords": [
"design",
"ui",
"frames",
"components"
]
}

@@ -13,5 +13,2 @@ {

"installHint": "Each path is exposed read/write. Add as many as you need, separated by spaces.",
"kind": "mcp",
"runtimeAvailability": "local",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -18,0 +15,0 @@ {

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -14,5 +14,2 @@ {

"installHint": "Runs the official Python server via uvx — no setup beyond the path.",
"kind": "mcp",
"runtimeAvailability": "local",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -19,0 +16,0 @@ {

@@ -5,16 +5,40 @@ {

"description": "Search code, manage issues and pull requests, and inspect repos via the GitHub API.",
"categories": [
"Engineering",
"Source control"
],
"appUrl": "https://github.com",
"docsUrl": "https://github.com/github/github-mcp-server",
"iconBg": "var(--oh-surface)",
"keywords": [
"git",
"pr",
"repo",
"issues",
"code"
],
"notes": "Current managed connector works with bearer tokens today; GitHub OAuth is a strong candidate for a future one-click connect flow.",
"popularityRank": 100,
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [
{
"id": "oauth",
"provider": "mcp",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"scopes": [
"read:user",
"repo"
]
}
},
"transport": {
"kind": "shttp",
"url": "https://api.githubcopilot.com/mcp/"
},
"http": {
"apiBaseUrl": "https://api.github.com",
"defaultTool": {
"name": "get_authenticated_user",
"description": "Fetch the authenticated GitHub user profile.",
"method": "GET",
"path": "/user"
}
}
},
{
"id": "api",

@@ -35,3 +59,12 @@ "provider": "mcp",

}
],
"iconBg": "var(--oh-surface)",
"logoUrl": "https://cdn.simpleicons.org/github/FFFFFF",
"keywords": [
"git",
"pr",
"repo",
"issues",
"code"
]
}

@@ -7,2 +7,3 @@ {

"iconBg": "#FFD21E",
"logoUrl": "https://cdn.simpleicons.org/huggingface/000000",
"iconColor": "#000000",

@@ -16,4 +17,2 @@ "keywords": [

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -20,0 +19,0 @@ {

{
"id": "kagi",
"name": "Kagi Search",
"description": "Paid, privacy-first search with high signal-to-noise — great for research.",
"description": "Paid, privacy-first search with high signal-to-noise \u2014 great for research.",
"docsUrl": "https://github.com/kagisearch/kagimcp",
"iconBg": "#FFB319",
"logoUrl": "https://cdn.simpleicons.org/kagi/000000",
"iconColor": "var(--oh-surface-deep)",

@@ -13,4 +14,2 @@ "keywords": [

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +16,0 @@ {

@@ -5,29 +5,60 @@ {

"description": "Browse and update Linear issues, cycles, and projects from the agent.",
"docsUrl": "https://linear.app/changelog/2025-05-01-mcp",
"iconBg": "#5E6AD2",
"keywords": [
"issues",
"project management",
"tasks",
"tickets"
"categories": [
"Project management",
"Engineering"
],
"appUrl": "https://linear.app",
"docsUrl": "https://linear.app/docs/mcp",
"notes": "Popular among modern product engineering teams and already useful to this repo's users.",
"popularityRank": 86,
"installHint": "Linear's hosted MCP server uses your Linear OAuth login — no key required.",
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://linear.app/oauth/authorize",
"tokenUrl": "https://api.linear.app/oauth/token",
"scopes": [
"read"
]
}
},
"http": {
"apiBaseUrl": "https://api.linear.app",
"defaultTool": {
"name": "list_issues",
"description": "Query issues from Linear via GraphQL.",
"method": "POST",
"path": "/graphql"
}
}
},
{
"id": "api-key",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://mcp.linear.app/sse",
"kind": "shttp",
"url": "https://mcp.linear.app/mcp",
"apiKeyOptional": true
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "bearer",
"apiKeyOptional": true,
"credentialLabel": "Linear API key",
"credentialPlaceholder": "Paste your Linear API key",
"credentialHelp": "Optional when the endpoint accepts your OAuth session; otherwise sent as Authorization: Bearer <token>."
}
}
]
],
"iconBg": "#5E6AD2",
"logoUrl": "https://cdn.simpleicons.org/linear/FFFFFF",
"keywords": [
"issues",
"project management",
"tasks",
"tickets"
],
"installHint": "Authenticate with a Linear API key (Linear \u2192 Settings \u2192 Security & access) \u2014 sent as a Bearer token. Optional when the endpoint accepts your OAuth session."
}

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#00684A",
"logoUrl": "https://cdn.simpleicons.org/mongodb/FFFFFF",
"keywords": [

@@ -13,4 +14,2 @@ "database",

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +16,0 @@ {

@@ -13,4 +13,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +15,0 @@ {

@@ -5,16 +5,34 @@ {

"description": "Read and edit Notion pages, databases, and blocks via Notion's MCP server.",
"categories": [
"Knowledge base",
"Documentation"
],
"appUrl": "https://www.notion.so",
"docsUrl": "https://developers.notion.com/docs/mcp",
"iconBg": "#FFFFFF",
"iconColor": "#000000",
"keywords": [
"docs",
"notes",
"wiki",
"knowledge base"
],
"notes": "",
"popularityRank": 82,
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://api.notion.com/v1/oauth/authorize",
"tokenUrl": "https://api.notion.com/v1/oauth/token",
"scopes": []
}
},
"http": {
"apiBaseUrl": "https://api.notion.com",
"openApiUrl": "https://developers.notion.com/openapi.json",
"defaultTool": {
"name": "post_search",
"description": "Search pages and databases in the connected Notion workspace.",
"method": "POST",
"path": "/v1/search"
}
}
},
{
"id": "api",

@@ -46,3 +64,12 @@ "provider": "mcp",

}
],
"iconBg": "#FFFFFF",
"logoUrl": "https://cdn.simpleicons.org/notion/000000",
"iconColor": "#000000",
"keywords": [
"docs",
"notes",
"wiki",
"knowledge base"
]
}

@@ -7,2 +7,3 @@ {

"iconBg": "#7C3AED",
"logoUrl": "https://cdn.simpleicons.org/obsidian/FFFFFF",
"keywords": [

@@ -14,5 +15,2 @@ "notes",

],
"kind": "mcp",
"runtimeAvailability": "local",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -19,0 +17,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#003087",
"logoUrl": "https://cdn.simpleicons.org/paypal/FFFFFF",
"keywords": [

@@ -13,16 +14,12 @@ "payments",

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "mcp",
"transport": {
"kind": "sse",
"url": "https://mcp.paypal.com/sse",
"apiKeyOptional": true
"kind": "shttp",
"url": "https://mcp.paypal.com/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2"
}

@@ -29,0 +26,0 @@ }

@@ -14,4 +14,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -18,0 +16,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "#DC382D",
"logoUrl": "https://cdn.simpleicons.org/redis/FFFFFF",
"keywords": [

@@ -13,5 +14,2 @@ "cache",

],
"kind": "mcp",
"runtimeAvailability": "local",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -18,0 +16,0 @@ {

@@ -7,2 +7,3 @@ {

"iconBg": "var(--oh-surface-deep)",
"logoUrl": "https://cdn.simpleicons.org/resend/FFFFFF",
"keywords": [

@@ -13,4 +14,2 @@ "email",

],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -17,0 +16,0 @@ {

@@ -5,27 +5,65 @@ {

"description": "Triage issues, inspect events, and run Seer fixes against your Sentry org.",
"categories": [
"Observability",
"Engineering"
],
"appUrl": "https://sentry.io",
"docsUrl": "https://docs.sentry.io/product/sentry-mcp/",
"iconBg": "#362D59",
"keywords": [
"errors",
"observability",
"monitoring",
"crash"
],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"notes": "Natural target for engineering support agents.",
"popularityRank": 36,
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://sentry.io/oauth/authorize",
"tokenUrl": "https://sentry.io/oauth/token",
"scopes": [
"project:read",
"event:read",
"org:read"
]
}
},
"http": {
"apiBaseUrl": "https://sentry.io/api/0",
"defaultTool": {
"name": "list_organizations",
"description": "List Sentry organizations available to the connected user.",
"method": "GET",
"path": "/organizations/"
}
}
},
{
"id": "oauth-mcp",
"provider": "mcp",
"transport": {
"kind": "shttp",
"url": "https://mcp.sentry.dev/mcp",
"apiKeyOptional": true
"url": "https://mcp.sentry.dev/mcp"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://sentry.io/oauth/authorize",
"tokenUrl": "https://sentry.io/oauth/token",
"scopes": [
"project:read",
"event:read",
"org:read"
]
}
}
}
],
"iconBg": "#362D59",
"logoUrl": "https://cdn.simpleicons.org/sentry/FFFFFF",
"keywords": [
"errors",
"observability",
"monitoring",
"crash"
]
}

@@ -12,4 +12,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -16,0 +14,0 @@ {

@@ -5,15 +5,46 @@ {

"description": "Read channels, post messages, and search workspace history from your agent.",
"categories": [
"Communication",
"Operations"
],
"appUrl": "https://slack.com",
"docsUrl": "https://github.com/zencoderai/slack-mcp-server",
"iconBg": "#4A154B",
"keywords": [
"chat",
"messaging",
"team"
],
"notes": "Uses Slack's official hosted MCP server with confidential OAuth user-token auth.",
"popularityRank": 95,
"installHint": "Create a Slack app with the required scopes, then paste its bot token below.",
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [
{
"id": "oauth",
"provider": "mcp",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://slack.com/oauth/v2_user/authorize",
"tokenUrl": "https://slack.com/api/oauth.v2.user.access",
"scopes": [
"search:read.public",
"search:read.private",
"search:read.mpim",
"search:read.im",
"search:read.files",
"search:read.users",
"chat:write",
"channels:history",
"groups:history",
"mpim:history",
"im:history",
"canvases:read",
"canvases:write",
"users:read",
"users:read.email"
],
"pkce": true,
"clientAuthentication": "body"
}
},
"transport": {
"kind": "shttp",
"url": "https://mcp.slack.com/mcp"
}
},
{
"id": "api",

@@ -52,3 +83,11 @@ "provider": "mcp",

}
]
],
"iconBg": "#4A154B",
"logoUrl": "https://cdn.simpleicons.org/slack/FFFFFF",
"keywords": [
"chat",
"messaging",
"team"
],
"installHint": "Create a Slack app with the required scopes, then paste its bot token below."
}

@@ -5,27 +5,57 @@ {

"description": "Query customers, payments, subscriptions, and invoices via Stripe's hosted MCP server.",
"categories": [
"Payments",
"Finance"
],
"appUrl": "https://stripe.com",
"docsUrl": "https://stripe.com/docs/mcp",
"iconBg": "#635BFF",
"keywords": [
"payments",
"billing",
"subscriptions",
"finance"
],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"notes": "High-value fintech automation target with mature OAuth patterns.",
"popularityRank": 28,
"connectionOptions": [
{
"id": "none",
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://connect.stripe.com/oauth/authorize",
"tokenUrl": "https://connect.stripe.com/oauth/token",
"scopes": [
"read_only"
]
}
},
"http": {
"apiBaseUrl": "https://api.stripe.com/v1",
"defaultTool": {
"name": "list_customers",
"description": "List Stripe customers from the connected account.",
"method": "GET",
"path": "/customers"
}
}
},
{
"id": "api",
"provider": "mcp",
"transport": {
"kind": "shttp",
"url": "https://mcp.stripe.com/",
"apiKeyOptional": true
"url": "https://mcp.stripe.com/"
},
"auth": {
"strategy": "none",
"apiKeyOptional": true
"strategy": "bearer",
"credentialLabel": "Stripe restricted key",
"credentialPlaceholder": "rk_live_...",
"credentialHelp": "Create a Stripe restricted key for MCP access and paste it here."
}
}
],
"iconBg": "#635BFF",
"logoUrl": "https://cdn.simpleicons.org/stripe/FFFFFF",
"keywords": [
"payments",
"billing",
"subscriptions",
"finance"
]
}

@@ -5,14 +5,33 @@ {

"description": "Query and manage your Supabase project, including database, auth, and storage.",
"categories": [
"Database",
"Developer tools"
],
"appUrl": "https://supabase.com",
"docsUrl": "https://supabase.com/docs/guides/getting-started/mcp",
"iconBg": "#3ECF8E",
"keywords": [
"database",
"auth",
"storage",
"postgres"
],
"kind": "mcp",
"defaultConnectionOptionId": "api",
"notes": "Popular developer platform with broad automation value and a documented management-API OAuth integration flow.",
"popularityRank": 38,
"connectionOptions": [
{
"id": "oauth",
"provider": "http",
"auth": {
"strategy": "oauth2",
"oauth": {
"authorizationUrl": "https://api.supabase.com/v1/oauth/authorize",
"tokenUrl": "https://api.supabase.com/v1/oauth/token",
"scopes": []
}
},
"http": {
"apiBaseUrl": "https://api.supabase.com/v1",
"defaultTool": {
"name": "list_projects",
"description": "List Supabase projects available to the connected user.",
"method": "GET",
"path": "/projects"
}
}
},
{
"id": "api",

@@ -43,3 +62,11 @@ "provider": "mcp",

}
],
"iconBg": "#3ECF8E",
"logoUrl": "https://cdn.simpleicons.org/supabase/FFFFFF",
"keywords": [
"database",
"auth",
"storage",
"postgres"
]
}

@@ -15,4 +15,2 @@ {

"installHint": "Paste your Tavily API key - the official tavily-mcp package runs via npx.",
"kind": "mcp",
"defaultConnectionOptionId": "api",
"connectionOptions": [

@@ -19,0 +17,0 @@ {

@@ -12,4 +12,2 @@ {

],
"kind": "mcp",
"defaultConnectionOptionId": "none",
"connectionOptions": [

@@ -16,0 +14,0 @@ {

@@ -91,48 +91,5 @@ export type MarketplaceFieldType = "text" | "password";

export interface OAuthProviderRegistrationDefaults {
provider?: IntegrationProvider;
authModes?: IntegrationAuthStrategy[];
authStrategy?: IntegrationAuthStrategy;
credentialLabel?: string;
credentialPlaceholder?: string;
credentialHelp?: string;
apiKeyHeaderName?: string;
apiBaseUrl?: string;
serverUrl?: string;
openApiUrl?: string;
authorizationUrl?: string;
tokenUrl?: string;
scopes?: string[];
optionalScopes?: string[];
toolScopes?: string[];
scopeSeparator?: "space" | "comma";
pkce?: boolean;
clientAuthentication?: "basic" | "body" | "none";
registrationUrl?: string;
additionalAuthorizationParams?: Record<string, string>;
additionalTokenParams?: Record<string, string>;
toolName?: string;
toolDescription?: string;
requestMethod?: string;
requestPath?: string;
}
export interface OAuthProviderCatalogOption {
slug: string;
name: string;
description: string;
categories: string[];
authStrategy: IntegrationAuthStrategy;
availability: "oauth_ready" | "manual_token" | "planned";
managedConnectorSlug?: string;
appUrl?: string;
docsUrl?: string;
notes: string;
popularityRank: number;
registrationDefaults?: OAuthProviderRegistrationDefaults;
}
export interface IntegrationCatalogEntry {
id: string;
kind: IntegrationProvider;
name: string;

@@ -146,26 +103,33 @@ description: string;

iconColor?: string;
logoUrl?: string;
keywords?: string[];
popularityRank?: number;
runtimeAvailability?: "all" | "local";
catalogStatus?: "oauth_ready" | "manual_token" | "planned";
managedConnectorSlug?: string;
authStrategy?: IntegrationAuthStrategy;
installHint?: string;
defaultConnectionOptionId?: string;
connectionOptions: IntegrationConnectionOption[];
registrationDefaults?: OAuthProviderRegistrationDefaults;
}
/**
* Filter for {@link listIntegrationCatalog}. Each dimension is tri-state:
* `true` keeps only entries that support that connector type, `false` keeps
* only entries that do not, and `undefined` leaves that dimension unfiltered.
*/
export interface IntegrationCatalogFilter {
/** Filter on whether the entry exposes at least one `mcp` connector. */
mcp?: boolean;
/** Filter on whether the entry exposes at least one `oauth2` connector. */
oauth?: boolean;
}
export const INTEGRATION_CATALOG: IntegrationCatalogEntry[];
export function listOAuthProviderCatalog(): OAuthProviderCatalogOption[];
export function getOAuthProviderRegistrationDefaults(
slug: string,
): OAuthProviderRegistrationDefaults | undefined;
export const hubspotMcpServerUrl: string;
export const hubspotMcpAuthorizationUrl: string;
export const hubspotMcpTokenUrl: string;
export const hubspotRequiredScopes: readonly string[];
export const hubspotOptionalScopes: readonly string[];
/**
* Return the full integration catalog, optionally filtered by connector type.
* Reads the generated static import index over `integrations/catalog/<id>.json`.
* Returns the cached array; callers must treat it as read-only.
*/
export function listIntegrationCatalog(
filter?: IntegrationCatalogFilter,
): IntegrationCatalogEntry[];
export function getIntegrationCatalogEntry(
id: string,
): IntegrationCatalogEntry | undefined;
export default INTEGRATION_CATALOG;

@@ -1,212 +0,34 @@

import airtable from "./catalog/airtable.json" with { type: "json" };
import apify from "./catalog/apify.json" with { type: "json" };
import atlassian from "./catalog/atlassian.json" with { type: "json" };
import brave_search from "./catalog/brave-search.json" with { type: "json" };
import browser_mcp from "./catalog/browser-mcp.json" with { type: "json" };
import clickhouse from "./catalog/clickhouse.json" with { type: "json" };
import cloudflare_bindings from "./catalog/cloudflare-bindings.json" with { type: "json" };
import cloudflare_browser_rendering from "./catalog/cloudflare-browser-rendering.json" with { type: "json" };
import cloudflare_builds from "./catalog/cloudflare-builds.json" with { type: "json" };
import cloudflare_docs from "./catalog/cloudflare-docs.json" with { type: "json" };
import cloudflare_observability from "./catalog/cloudflare-observability.json" with { type: "json" };
import deepwiki from "./catalog/deepwiki.json" with { type: "json" };
import elevenlabs from "./catalog/elevenlabs.json" with { type: "json" };
import everything from "./catalog/everything.json" with { type: "json" };
import exa from "./catalog/exa.json" with { type: "json" };
import fetch from "./catalog/fetch.json" with { type: "json" };
import figma from "./catalog/figma.json" with { type: "json" };
import filesystem from "./catalog/filesystem.json" with { type: "json" };
import firecrawl from "./catalog/firecrawl.json" with { type: "json" };
import git from "./catalog/git.json" with { type: "json" };
import github from "./catalog/github.json" with { type: "json" };
import huggingface from "./catalog/huggingface.json" with { type: "json" };
import kagi from "./catalog/kagi.json" with { type: "json" };
import linear from "./catalog/linear.json" with { type: "json" };
import memory from "./catalog/memory.json" with { type: "json" };
import mongodb from "./catalog/mongodb.json" with { type: "json" };
import neon from "./catalog/neon.json" with { type: "json" };
import notion from "./catalog/notion.json" with { type: "json" };
import obsidian from "./catalog/obsidian.json" with { type: "json" };
import paypal from "./catalog/paypal.json" with { type: "json" };
import playwright from "./catalog/playwright.json" with { type: "json" };
import redis from "./catalog/redis.json" with { type: "json" };
import resend from "./catalog/resend.json" with { type: "json" };
import sentry from "./catalog/sentry.json" with { type: "json" };
import sequential_thinking from "./catalog/sequential-thinking.json" with { type: "json" };
import slack from "./catalog/slack.json" with { type: "json" };
import stripe from "./catalog/stripe.json" with { type: "json" };
import supabase from "./catalog/supabase.json" with { type: "json" };
import tavily from "./catalog/tavily.json" with { type: "json" };
import time from "./catalog/time.json" with { type: "json" };
import { listOAuthProviderCatalog } from "./oauth-provider-catalog.js";
export { listOAuthProviderCatalog } from "./oauth-provider-catalog.js";
export {
getOAuthProviderRegistrationDefaults,
hubspotMcpAuthorizationUrl,
hubspotMcpServerUrl,
hubspotMcpTokenUrl,
hubspotOptionalScopes,
hubspotRequiredScopes,
} from "./oauth-provider-registration-defaults.js";
/**
* Runtime integration catalog.
*
* The source of truth is the hand-authored `integrations/catalog/<id>.json`
* directory. `catalog-index.js` is generated from that directory so the JS
* package can statically import each JSON file without an aggregate JSON asset.
*/
import { INTEGRATION_CATALOG_ENTRIES } from "./catalog-index.js";
const DIRECT_INTEGRATIONS = [
airtable,
apify,
atlassian,
brave_search,
browser_mcp,
clickhouse,
cloudflare_bindings,
cloudflare_browser_rendering,
cloudflare_builds,
cloudflare_docs,
cloudflare_observability,
deepwiki,
elevenlabs,
everything,
exa,
fetch,
figma,
filesystem,
firecrawl,
git,
github,
huggingface,
kagi,
linear,
memory,
mongodb,
neon,
notion,
obsidian,
paypal,
playwright,
redis,
resend,
sentry,
sequential_thinking,
slack,
stripe,
supabase,
tavily,
time,
];
const INTEGRATIONS = INTEGRATION_CATALOG_ENTRIES;
const INTEGRATION_BY_ID = new Map(INTEGRATIONS.map((entry) => [entry.id, entry]));
const optionIdForDefaults = (defaults) => {
const strategy = defaults?.authStrategy ?? (defaults?.authorizationUrl || defaults?.tokenUrl ? "oauth2" : "oauth2");
if (strategy === "oauth2") return "oauth";
if (strategy === "none") return "none";
return "api";
};
const entrySupportsMcp = (entry) =>
entry.connectionOptions.some((option) => option.provider === "mcp");
const providerConnectionOption = (provider) => {
const defaults = provider.registrationDefaults;
if (!defaults) return null;
const option = {
id: optionIdForDefaults(defaults),
provider: defaults.provider ?? (defaults.serverUrl ? "mcp" : "http"),
auth: {
strategy: defaults.authStrategy ?? provider.authStrategy ?? "oauth2",
authModes: defaults.authModes,
credentialLabel: defaults.credentialLabel,
credentialPlaceholder: defaults.credentialPlaceholder,
credentialHelp: defaults.credentialHelp,
apiKeyHeaderName: defaults.apiKeyHeaderName,
oauth: defaults.authorizationUrl || defaults.tokenUrl ? {
authorizationUrl: defaults.authorizationUrl,
tokenUrl: defaults.tokenUrl,
scopes: defaults.scopes ?? [],
optionalScopes: defaults.optionalScopes,
toolScopes: defaults.toolScopes,
scopeSeparator: defaults.scopeSeparator,
pkce: defaults.pkce,
clientAuthentication: defaults.clientAuthentication,
registrationUrl: defaults.registrationUrl,
additionalAuthorizationParams: defaults.additionalAuthorizationParams,
additionalTokenParams: defaults.additionalTokenParams,
} : undefined,
},
};
const entrySupportsOauth = (entry) =>
entry.connectionOptions.some((option) => option.auth?.strategy === "oauth2");
if (option.provider === "mcp") {
option.transport = { kind: "shttp", url: defaults.serverUrl };
} else {
option.http = {
apiBaseUrl: defaults.apiBaseUrl,
openApiUrl: defaults.openApiUrl,
defaultTool: defaults.toolName ? {
name: defaults.toolName,
description: defaults.toolDescription,
method: defaults.requestMethod,
path: defaults.requestPath,
scopes: defaults.toolScopes,
} : undefined,
};
}
return option;
export const listIntegrationCatalog = (filter) => {
if (!filter) return INTEGRATIONS;
const { mcp, oauth } = filter;
if (mcp === undefined && oauth === undefined) return INTEGRATIONS;
return INTEGRATIONS.filter((entry) => {
const mcpOk = mcp === undefined || entrySupportsMcp(entry) === mcp;
const oauthOk = oauth === undefined || entrySupportsOauth(entry) === oauth;
return mcpOk && oauthOk;
});
};
const providerIntegration = (provider) => {
const option = providerConnectionOption(provider);
return {
id: provider.slug,
kind: option?.provider ?? "http",
name: provider.name,
description: provider.description,
categories: provider.categories,
appUrl: provider.appUrl,
docsUrl: provider.docsUrl,
notes: provider.notes,
catalogStatus: provider.availability,
managedConnectorSlug: provider.managedConnectorSlug,
authStrategy: provider.authStrategy,
popularityRank: provider.popularityRank,
registrationDefaults: provider.registrationDefaults,
...(option ? { defaultConnectionOptionId: option.id, connectionOptions: [option] } : { connectionOptions: [] }),
};
};
export const getIntegrationCatalogEntry = (id) => INTEGRATION_BY_ID.get(id);
const mergeOptions = (left = [], right = []) => {
const options = new Map();
for (const option of left) options.set(option.id, option);
for (const option of right) {
if (!options.has(option.id)) options.set(option.id, option);
}
return [...options.values()];
};
export const INTEGRATION_CATALOG = INTEGRATIONS;
const mergeIntegration = (base, override) => {
const connectionOptions = mergeOptions(base.connectionOptions, override.connectionOptions);
return {
...base,
...override,
catalogStatus: base.catalogStatus ?? override.catalogStatus,
managedConnectorSlug: base.managedConnectorSlug ?? override.managedConnectorSlug,
authStrategy: base.authStrategy ?? override.authStrategy,
registrationDefaults: base.registrationDefaults ?? override.registrationDefaults,
connectionOptions,
defaultConnectionOptionId:
base.defaultConnectionOptionId ?? override.defaultConnectionOptionId ?? connectionOptions[0]?.id,
};
};
const entriesById = new Map();
for (const provider of listOAuthProviderCatalog().map(providerIntegration)) {
entriesById.set(provider.id, provider);
}
for (const direct of DIRECT_INTEGRATIONS) {
entriesById.set(
direct.id,
entriesById.has(direct.id)
? mergeIntegration(entriesById.get(direct.id), direct)
: direct,
);
}
export const INTEGRATION_CATALOG = [...entriesById.values()].sort((a, b) => {
const rankDelta = (b.popularityRank ?? 0) - (a.popularityRank ?? 0);
return rankDelta || a.name.localeCompare(b.name);
});
export default INTEGRATION_CATALOG;

@@ -7,6 +7,16 @@ # Integration catalog

- `catalog/*.json` contains one source file per direct integration entry.
- `index.js` assembles and exports the catalog for Node.js and bundlers.
- `catalog/<id>.json` is the hand-authored source of truth. Add or edit an
integration by changing exactly one file in that directory.
- `catalog-index.js` is generated from `catalog/*.json` so the JavaScript
package can statically import every individual JSON file without an aggregate
catalog JSON asset.
- The Python package includes the same individual `catalog/*.json` files and
reads them directly.
- `index.js` derives the `supportsMcp`/`supportsOauth` filters from the
canonical connection options at read time.
- `index.d.ts` contains the public TypeScript shape.
Each integration carries its OAuth/MCP connection data directly. Do not add a
separate provider catalog or per-language provider data.
Consumers can import the package export:

@@ -26,4 +36,4 @@

instead of `MCP_CATALOG` from `@openhands/extensions/mcps`.
- Import logo mappings from `@openhands/extensions/integrations/logos`
instead of `@openhands/extensions/mcps/logos`.
- Read serializable logo metadata from each `IntegrationCatalogEntry` (`logoUrl`,
`iconBg`, and `iconColor`) instead of importing React-specific logo maps.
- Use `IntegrationCatalogEntry` instead of `McpCatalogEntry`.

@@ -33,4 +43,4 @@ - Read MCP configuration from `entry.connectionOptions[]`. Direct MCP entries

options such as `id: "oauth"` for a hosted OAuth MCP endpoint and `id:
"api"` for an API-key or stdio fallback.
- Use `entry.defaultConnectionOptionId` to choose the preferred option.
"api"` for an API-key or stdio fallback. The first option is the preferred
default.
- Automation catalog entries now use `requiredIntegrationIds` instead of

@@ -42,2 +52,2 @@ `requiredMcpIds`.

The catalog intentionally stores only serializable data. Client applications are responsible for mapping entries to UI-specific icons or styling.
The catalog intentionally stores only serializable data, including language-agnostic logo URLs and optional presentation colors. Client applications can render those fields directly while keeping any purely UI-specific styling local.

@@ -92,2 +92,15 @@ {

{
"name": "agent-canvas-environment",
"source": "./skills/agent-canvas-environment",
"description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, safe workspace hygiene, and local conversation delegation.",
"category": "development",
"keywords": [
"agent-canvas",
"openhands",
"local",
"conversation",
"delegation"
]
},
{
"name": "openhands-sdk",

@@ -408,3 +421,3 @@ "source": "./skills/openhands-sdk",

"source": "./skills/openhands-api",
"description": "Use the OpenHands Cloud REST API (V1) to create and manage app conversations, including multi-conversation delegation workflows, and to access sandbox agent-server endpoints. Includes minimal Python and TypeScript clients under scripts/.",
"description": "Use the OpenHands Cloud REST API (V1) and agent-server APIs to create and manage Cloud or local backend conversations, including multi-conversation delegation workflows. Includes minimal Python and TypeScript clients under scripts/.",
"category": "development",

@@ -411,0 +424,0 @@ "keywords": [

{
"name": "@openhands/extensions",
"version": "0.5.0",
"version": "0.6.0",
"description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",

@@ -12,3 +12,5 @@ "license": "MIT",

"scripts": {
"build:skills": "node scripts/build-skills-catalog.mjs"
"build:skills": "node scripts/build-skills-catalog.mjs",
"build:integrations": "node scripts/build-integration-catalog.mjs",
"build": "npm run build:skills && npm run build:integrations"
},

@@ -43,7 +45,2 @@ "engines": {

},
"./integrations/logos": {
"types": "./integrations/logos.d.ts",
"import": "./integrations/logos.js",
"default": "./integrations/logos.js"
},
"./integrations/catalog/*.json": "./integrations/catalog/*.json",

@@ -50,0 +47,0 @@ "./skills": {

@@ -39,7 +39,7 @@ # COBOL Modernization Plugin

│ └── SKILL.md
├── cobol-modernization-overview/ # Plugin overview
├── cobol-modernization/ # Plugin overview
│ └── SKILL.md
├── mainframe-planning/ # Phase 2: Transformation planning
│ └── SKILL.md
├── mainfraime-removal/ # Phase 3: Mainframe dependency removal
├── mainframe-removal/ # Phase 3: Mainframe dependency removal
│ ├── SKILL.md

@@ -143,3 +143,3 @@ │ └── references/

- See [skills/mainframe-planning/SKILL.md](skills/mainframe-planning/SKILL.md) for planning
- See [skills/mainfraime-removal/SKILL.md](skills/mainfraime-removal/SKILL.md) for mainframe removal
- See [skills/mainframe-removal/SKILL.md](skills/mainframe-removal/SKILL.md) for mainframe removal
- See [skills/to-java-migration/SKILL.md](skills/to-java-migration/SKILL.md) for Java translation

@@ -146,0 +146,0 @@

---
name: magic-word
description: A test skill that responds to the magic word "alakazam" with a specific phrase

@@ -3,0 +4,0 @@ triggers:

@@ -37,3 +37,3 @@ # Migration Scoring Plugin

└── skills/ # Workflow phase skills
├── migration-scoring-overview/ # Plugin overview
├── migration-scoring/ # Plugin overview
│ └── SKILL.md

@@ -40,0 +40,0 @@ ├── migration-mapping/ # Phase 1: Source-to-target mapping

[project]
name = "extensions"
version = "0.5.0"
description = "OpenHands extensions, plugins, and skills"
name = "openhands-extensions"
version = "0.6.0"
description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling>=1.21"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["python/openhands_extensions"]
[tool.hatch.build.targets.wheel.force-include]
"integrations/catalog" = "openhands_extensions/catalog"
[dependency-groups]

@@ -13,1 +23,5 @@ test = [

]
[tool.pytest.ini_options]
pythonpath = ["python"]
testpaths = ["tests"]

@@ -60,6 +60,33 @@ # OpenHands Extensions

### Python Package
The integration catalog is also published as a Python package (`openhands-extensions`) so Python services read the same catalog data as JS consumers. The single source of truth is the hand-authored JSON asset `integrations/integration-catalog.json` (NOT generated from any `.mjs`/`.js` source). Each per-integration entry also exists as `integrations/catalog/<id>.json`; a CI parity test asserts the two never drift. Both the JS package (`@openhands/extensions/integrations`) and the Python package read that same JSON asset at runtime, so the two language bindings can never drift.
The catalog is one array where each entry can carry oauth and/or mcp/http `connectionOptions`. `supportsOauth` / `supportsMcp` flags are derived at runtime. Use `listIntegrationCatalog({ mcp, oauth })` (JS) or `list_integration_catalog(mcp=, oauth=)` (Python) to filter by connector type - for example only integrations that support an oauth connector.
```python
from openhands_extensions import (
list_integration_catalog, # listIntegrationCatalog({ mcp, oauth })
get_integration_catalog_entry, # getIntegrationCatalogEntry(id)
INTEGRATION_CATALOG_SNAPSHOT, # { integrations }
)
all_integrations = list_integration_catalog()
oauth_integrations = list_integration_catalog(oauth=True) # only entries with an oauth connector
mcp_integrations = list_integration_catalog(mcp=True) # only entries with an mcp connector
hubspot = get_integration_catalog_entry("hubspot")
```
Install from git (the hub backend consumes it this way):
```bash
pip install git+https://github.com/OpenHands/extensions.git
```
The JS and Python versions are kept in lock-step by `release-please` and guarded by `tests/test_version_alignment.py`.
## Extensions Catalog
<!-- BEGIN AUTO-GENERATED CATALOG -->
This repository contains **2 marketplace(s)** with **57 extensions** (47 skills, 10 plugins).
This repository contains **2 marketplace(s)** with **58 extensions** (48 skills, 10 plugins).

@@ -83,3 +110,3 @@ ### large-codebase

**53 extensions** (45 skills, 8 plugins)
**54 extensions** (46 skills, 8 plugins)

@@ -89,2 +116,3 @@ | Name | Type | Description | Commands |

| add-skill | skill | Add (import) an OpenHands skill from a GitHub repository into the current workspace. | — |
| agent-canvas-environment | skill | Work effectively inside a local Agent Canvas environment, including local agent-server auth, safe workspace hygiene, ... | — |
| agent-creator | skill | Create file-based sub-agents as Markdown files — no Python code required. Guides the user through a structured interv... | `/agent-creator` |

@@ -123,3 +151,3 @@ | agent-memory | skill | Persist and retrieve repository-specific knowledge using AGENTS.md files. Use when you want to save important informa... | `/remember` |

| openhands | plugin | Unified OpenHands plugin — bundles Cloud CLI, REST API (openhands-api), and Automations (openhands-automation) into a... | `/openhands-cloud` |
| openhands-api | skill | Use the OpenHands Cloud REST API (V1) to create and manage app conversations, including multi-conversation delegation... | — |
| openhands-api | skill | Use the OpenHands Cloud REST API (V1) and agent-server APIs to create and manage Cloud or local backend conversations... | — |
| openhands-automation | skill | Create and manage OpenHands automations - scheduled tasks that run in sandboxes. Use the prompt preset to create auto... | `/automation:create` |

@@ -126,0 +154,0 @@ | openhands-sdk | skill | Reference skill for the OpenHands Software Agent SDK - build AI agents with custom tools, LLM configuration, conversa... | `/sdk` |

@@ -12,3 +12,4 @@ {

"extra-files": [
{ "type": "toml", "path": "pyproject.toml", "jsonpath": "$.project.version" }
{ "type": "toml", "path": "pyproject.toml", "jsonpath": "$.project.version" },
"python/openhands_extensions/_version.py"
]

@@ -15,0 +16,0 @@ }

@@ -1,4 +0,4 @@

# State File Schema
# State Schema
The automation maintains a JSON state file that persists across polling runs.
The automation maintains a JSON state document that persists across polling runs.
It is the source of truth for which trigger-label events have queued reviews

@@ -9,4 +9,11 @@ and which conversations are still active.

## File Location
## Storage
**Primary (cloud):** The state is stored in the automation service's built-in KV
store under the key `"state"`. The KV store is available when `AUTOMATION_KV_TOKEN`
is injected into the run environment. Each automation has its own isolated namespace.
**Fallback (local/dev):** When the KV store is not available, the state is written
to a local JSON file at:
```

@@ -120,5 +127,12 @@ {WORKSPACE_BASE_ROOT}/automation-state/github_pr_reviewer_label_event_{automation_id}.json

To force the automation to reconsider previous label events, delete the state
file:
from the KV store (cloud) or the fallback file (local).
**Cloud (KV store):**
```bash
curl -X DELETE "${OPENHANDS_HOST}/api/automation/v1/kv/state" \
-H "Authorization: Bearer ${AUTOMATION_KV_TOKEN}"
```
**Local (file fallback):**
```bash
rm ~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_<id>.json

@@ -125,0 +139,0 @@ ```

@@ -70,2 +70,41 @@ """

# ── State persistence (KV store with local-file fallback) ─────────────────────
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
_STATE_KEY = "state"
def _kv_available() -> bool:
return bool(_KV_TOKEN and _KV_BASE)
def _kv_get(key: str) -> dict | None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def _kv_set(key: str, value: dict) -> None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
data=json.dumps(value).encode(),
headers={
"Authorization": f"Bearer {_KV_TOKEN}",
"Content-Type": "application/json",
},
method="PUT",
)
with urllib.request.urlopen(req) as r:
r.read()
def _state_file_path() -> str:

@@ -86,9 +125,3 @@ workspace_base = os.environ.get("WORKSPACE_BASE", "")

def load_state(path: str) -> dict:
if os.path.exists(path):
try:
with open(path) as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as exc:
print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
def _default_state() -> dict:
return {

@@ -103,3 +136,25 @@ "version": 2,

def save_state(path: str, state: dict) -> None:
def load_state() -> dict:
if _kv_available():
data = _kv_get(_STATE_KEY)
if data is not None:
print("State loaded from KV store")
return data
return _default_state()
path = _state_file_path()
if os.path.exists(path):
try:
with open(path) as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as exc:
print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
return _default_state()
def save_state(state: dict) -> None:
if _kv_available():
_kv_set(_STATE_KEY, state)
print("State saved to KV store")
return
path = _state_file_path()
tmp_path = f"{path}.tmp"

@@ -109,2 +164,3 @@ with open(tmp_path, "w") as f:

os.replace(tmp_path, path)
print(f"State saved to {path}")

@@ -529,4 +585,3 @@

def main() -> str | None:
state_path = _state_file_path()
state = load_state(state_path)
state = load_state()
agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")

@@ -599,4 +654,3 @@ api_key = _get_env_key()

state["updated_at"] = time.time()
save_state(state_path, state)
print(f"State saved → {state_path}")
save_state(state)
return last_conversation_id

@@ -603,0 +657,0 @@

@@ -1,5 +0,5 @@

# State File Schema
# State Schema
The automation maintains a JSON state file that persists across polling runs.
This file is the source of truth for which conversations are active, which
The automation maintains a JSON state document that persists across polling runs.
This document is the source of truth for which conversations are active, which
comment IDs have already been processed, and the timestamp of the last poll.

@@ -9,4 +9,11 @@

## File Location
## Storage
**Primary (cloud):** The state is stored in the automation service's built-in KV
store under the key `"state"`. The KV store is available when `AUTOMATION_KV_TOKEN`
is injected into the run environment. Each automation has its own isolated namespace.
**Fallback (local/dev):** When the KV store is not available, the state is written
to a local JSON file at:
```

@@ -13,0 +20,0 @@ {WORKSPACE_BASE_ROOT}/automation-state/github_poller_{automation_id}.json

@@ -111,10 +111,43 @@ """

# ── State management ───────────────────────────────────────────────────────────
# ── State persistence (KV store with local-file fallback) ─────────────────────
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
_STATE_KEY = "state"
def _kv_available() -> bool:
return bool(_KV_TOKEN and _KV_BASE)
def _kv_get(key: str) -> dict | None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def _kv_set(key: str, value: dict) -> None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
data=json.dumps(value).encode(),
headers={
"Authorization": f"Bearer {_KV_TOKEN}",
"Content-Type": "application/json",
},
method="PUT",
)
with urllib.request.urlopen(req) as r:
r.read()
def _state_file_path() -> str:
"""Derive a persistent storage path from WORKSPACE_BASE.
WORKSPACE_BASE = {root}/automation-runs/{run_id}
State lives two levels up at {root}/automation-state/.
"""
"""Derive a persistent storage path from WORKSPACE_BASE (file fallback only)."""
workspace_base = os.environ.get("WORKSPACE_BASE", "")

@@ -141,3 +174,20 @@ event_payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))

def load_state(path: str) -> dict:
def _default_state() -> dict:
return {
"version": 1,
"repo": REPO,
"last_poll": _default_since(),
"conversations": {},
"processed_comment_ids": [],
}
def load_state() -> dict:
if _kv_available():
data = _kv_get(_STATE_KEY)
if data is not None:
print("State loaded from KV store")
return data
return _default_state()
path = _state_file_path()
if os.path.exists(path):

@@ -149,14 +199,14 @@ try:

print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
return {
"version": 1,
"repo": REPO,
"last_poll": _default_since(),
"conversations": {}, # issue_number (str) → ConversationRecord
"processed_comment_ids": [], # list of int comment IDs already handled
}
return _default_state()
def save_state(path: str, state: dict) -> None:
def save_state(state: dict) -> None:
if _kv_available():
_kv_set(_STATE_KEY, state)
print("State saved to KV store")
return
path = _state_file_path()
with open(path, "w") as f:
json.dump(state, f, indent=2)
print(f"State saved to {path}")

@@ -803,4 +853,3 @@

"""Run one polling cycle. Returns the last conversation ID created, if any."""
state_path = _state_file_path()
state = load_state(state_path)
state = load_state()

@@ -906,4 +955,3 @@ agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")

save_state(state_path, state)
print(f"State saved → {state_path}")
save_state(state)
return last_conversation_id

@@ -910,0 +958,0 @@

@@ -32,6 +32,16 @@ """Unit tests for github-repo-monitor main.py.

def _no_kv(path):
"""Return a pair of patches: KV disabled, file path pinned to *path*."""
return (
patch("main._kv_available", return_value=False),
patch("main._state_file_path", return_value=path),
)
class TestLoadState(unittest.TestCase):
def test_missing_file_returns_default(self):
state = main.load_state("/nonexistent/path/state.json")
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value="/nonexistent/path/state.json"):
state = main.load_state()
self.assertIn("conversations", state)

@@ -46,3 +56,5 @@ self.assertIn("processed_comment_ids", state)

try:
state = main.load_state(path)
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value=path):
state = main.load_state()
self.assertEqual(state["custom"], "value")

@@ -57,3 +69,5 @@ finally:

try:
state = main.load_state(path)
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value=path):
state = main.load_state()
# Should return the default state rather than raising.

@@ -70,3 +84,5 @@ self.assertIn("conversations", state)

try:
state = main.load_state(path)
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value=path):
state = main.load_state()
self.assertIn("conversations", state)

@@ -88,4 +104,6 @@ finally:

try:
main.save_state(path, data)
loaded = main.load_state(path)
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value=path):
main.save_state(data)
loaded = main.load_state()
self.assertEqual(loaded["conversations"]["42"]["conversation_id"], "abc")

@@ -97,2 +115,41 @@ self.assertEqual(loaded["processed_comment_ids"], [1, 2, 3])

class TestKVState(unittest.TestCase):
def test_load_reads_value_from_kv(self):
kv_data = {"version": 1, "conversations": {"7": {"conversation_id": "kv-c"}},
"processed_comment_ids": []}
with patch("main._kv_available", return_value=True), \
patch("main._kv_get", return_value=kv_data) as mock_get:
state = main.load_state()
self.assertEqual(state["conversations"]["7"]["conversation_id"], "kv-c")
mock_get.assert_called_once_with("state")
def test_load_returns_default_on_kv_miss(self):
with patch("main._kv_available", return_value=True), \
patch("main._kv_get", return_value=None):
state = main.load_state()
self.assertIn("conversations", state)
self.assertEqual(state["version"], 1)
def test_save_writes_to_kv(self):
data = {"version": 1, "conversations": {}, "processed_comment_ids": []}
with patch("main._kv_available", return_value=True), \
patch("main._kv_set") as mock_set:
main.save_state(data)
mock_set.assert_called_once_with("state", data)
def test_kv_error_propagates_on_load(self):
with patch("main._kv_available", return_value=True), \
patch("main._kv_get", side_effect=RuntimeError("network error")):
with self.assertRaises(RuntimeError):
main.load_state()
def test_kv_error_propagates_on_save(self):
data = {"version": 1, "conversations": {}, "processed_comment_ids": []}
with patch("main._kv_available", return_value=True), \
patch("main._kv_set", side_effect=RuntimeError("network error")):
with self.assertRaises(RuntimeError):
main.save_state(data)
# ── Bot detection tests ────────────────────────────────────────────────────────

@@ -223,4 +280,6 @@

"processed_comment_ids": [101, 202, 303]}
main.save_state(path, state)
loaded = main.load_state(path)
with patch("main._kv_available", return_value=False), \
patch("main._state_file_path", return_value=path):
main.save_state(state)
loaded = main.load_state()
self.assertIn(101, loaded["processed_comment_ids"])

@@ -227,0 +286,0 @@ self.assertIn(303, loaded["processed_comment_ids"])

# openhands-api
Reference skill + minimal clients for the **OpenHands Cloud API** (V1).
Reference skill + minimal clients for the **OpenHands Cloud API** (V1) and common **agent-server APIs**.
This skill now also covers the **multi-conversation delegation pattern**: start additional Cloud conversations when you want fresh context windows, background work, or parallel tasks.
It also covers direct local Agent Canvas backend conversations when you need to call a local agent server with `X-Session-API-Key` instead of creating a Cloud app conversation.
- Skill instructions and endpoint overview: [`SKILL.md`](./SKILL.md)

@@ -45,2 +47,4 @@ - Minimal Python client: [`scripts/openhands_api.py`](./scripts/openhands_api.py)

- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`
- `OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx`
- `OpenHands/docs/sdk/arch/agent-server.mdx`
- `OpenHands/docs/openhands/usage/api/v1.mdx`

@@ -47,0 +51,0 @@ - `OpenHands/OpenHands/openhands/app_server/v1_router.py`

---
name: openhands-api
description: Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work. Use when you need to automate common OpenHands Cloud actions; don't use for general sandbox/dev tasks unrelated to the OpenHands API.
description: Reference skill for the OpenHands Cloud REST API (V1) and agent-server APIs, including how to start additional cloud or local backend conversations for fresh-context or delegated work.
triggers:

@@ -13,5 +13,5 @@ - openhands-api

This skill documents the **OpenHands Cloud API** (V1) and provides small, easy-to-copy clients.
This skill documents the **OpenHands Cloud API** (V1), commonly used **agent-server APIs**, and small, easy-to-copy clients.
It is intentionally focused on common OpenHands Cloud workflows:
It is intentionally focused on common OpenHands API workflows:

@@ -22,2 +22,3 @@ - Defaults to OpenHands Cloud (`https://app.all-hands.dev`).

- Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.
- Covers **local Agent Canvas backend conversations**: start or inspect conversations by calling a local agent server directly.

@@ -33,2 +34,3 @@ ## When to use this skill

- access sandbox agent-server endpoints once a conversation is running
- start or inspect conversations on a local Agent Canvas backend or local agent server

@@ -79,2 +81,68 @@ ## Auth

### Local Agent Canvas backend
Use the local backend flow only for local Agent Canvas / agent-server development, such as `agent-canvas`, `agent-canvas --backend-only`, or `npm run dev` with ingress at `http://localhost:8000`. This calls the agent server directly with `X-Session-API-Key`. It is not an automation, and it is different from OpenHands Cloud delegation through `POST /api/v1/app-conversations`, which uses Bearer auth against the Cloud app API and may return asynchronous start-task records.
When Agent Canvas runs locally, the launcher uses `LOCAL_BACKEND_API_KEY` when it is set. Otherwise it generates and persists the session API key at `~/.openhands/agent-canvas/api-key.txt`. Set `OH_SESSION_API_KEY_PATH` to override the persisted key path. Never print, log, or paste the actual key; use command substitution or an environment variable in examples and scripts.
```bash
LOCAL_AGENT_SERVER_URL="${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}"
SESSION_API_KEY="${LOCAL_BACKEND_API_KEY:-$(cat "${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}")}"
```
Check the local server before creating a backend conversation:
```bash
curl -sS "${LOCAL_AGENT_SERVER_URL}/server_info" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
```
Start a backend conversation with `POST /api/conversations`. Include the agent settings and workspace expected by that backend. Local agent-server calls use an explicit `workspace` such as `{"kind": "LocalWorkspace", "working_dir": "/workspace"}`; Cloud app-conversation delegation instead uses app-server fields such as `selected_repository` and `selected_branch`. If you are starting the conversation from an existing Agent Canvas session, pass through the current configured settings or encrypted settings rather than hard-coding secrets into scripts.
```bash
CONVERSATION_JSON=$(curl -sS -X POST "${LOCAL_AGENT_SERVER_URL}/api/conversations" \
-H "X-Session-API-Key: ${SESSION_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"agent": {
"kind": "Agent",
"llm": {
"model": "your-model-provider/your-model-name",
"api_key": "**********"
},
"tools": [
{"name": "terminal"},
{"name": "file_editor"},
{"name": "task_tracker"}
]
},
"workspace": {"kind": "LocalWorkspace", "working_dir": "/workspace"},
"initial_message": {
"content": [{"text": "Summarize the current workspace."}],
"run": true
}
}
JSON
)
CONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"${CONVERSATION_JSON}")
printf 'Conversation: %s/api/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
```
Poll status and inspect recent events:
```bash
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
```
If the same base URL serves the Agent Canvas UI, the browser route is:
```bash
printf '%s/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
```
## Common V1 app server endpoints

@@ -396,5 +464,7 @@

This skill is aligned against the current V1 docs and implementation:
This skill is aligned against the current OpenHands API docs and implementation:
- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`
- `OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx`
- `OpenHands/docs/sdk/arch/agent-server.mdx`
- `OpenHands/docs/openhands/usage/api/v1.mdx`

@@ -404,2 +474,1 @@ - `OpenHands/OpenHands/openhands/app_server/v1_router.py`

- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py`

@@ -442,2 +442,166 @@ # Custom Automation Reference

## State Persistence (KV Store)
Polling automations that run on a schedule need to remember state between runs — for example, the timestamp of the last processed event, or which conversation IDs are currently active. Storing this in a local file does not work on cloud deployments where each run may land on a fresh pod.
The automation service provides a built-in key-value store scoped per-automation. It is available in every run when the service is configured with `AUTOMATION_KV_SECRET`. Detect availability by checking for `AUTOMATION_KV_TOKEN` in the environment.
### KV Store API
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/kv/{key}` | GET | Get value (404 if not found) |
| `/v1/kv/{key}` | PUT | Set value (201 on create, 200 on update) |
| `/v1/kv/{key}` | DELETE | Delete key |
| `/v1/kv/{key}/incr` | POST | Atomic integer increment |
| `/v1/kv/{key}/decr` | POST | Atomic integer decrement |
| `/v1/kv/{key}/rpush` | POST | Append item to a list |
| `/v1/kv/{key}/lpop` | POST | Pop item from front of a list |
**Authentication:** `Authorization: Bearer $AUTOMATION_KV_TOKEN`
**Base URL:** `$AUTOMATION_API_URL` (e.g., `https://app.all-hands.dev/api/automation`)
Values are arbitrary JSON (dict, list, number, string). All keys are isolated per-automation — different automations cannot access each other's data.
### KV Store Helpers
Copy these helpers into any deterministic script that needs state persistence:
```python
import json, os, urllib.error, urllib.request
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
def kv_available() -> bool:
"""Return True when the KV store is reachable in this run."""
return bool(_KV_TOKEN and _KV_BASE)
def kv_get(key: str):
"""Fetch a value from the KV store. Returns None if the key does not exist."""
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def kv_set(key: str, value) -> None:
"""Write a value to the KV store."""
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
data=json.dumps(value).encode(),
headers={
"Authorization": f"Bearer {_KV_TOKEN}",
"Content-Type": "application/json",
},
method="PUT",
)
with urllib.request.urlopen(req) as r:
r.read()
```
### Load / Save Pattern
For polling scripts that maintain a single state document, use a KV-first pattern with a local-file fallback so the script also works in local/dev environments where the KV store is not configured:
```python
import json, os, urllib.error, urllib.request
from pathlib import Path
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
_STATE_KEY = "state"
def kv_available() -> bool:
return bool(_KV_TOKEN and _KV_BASE)
def kv_get(key: str):
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def kv_set(key: str, value) -> None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
data=json.dumps(value).encode(),
headers={
"Authorization": f"Bearer {_KV_TOKEN}",
"Content-Type": "application/json",
},
method="PUT",
)
with urllib.request.urlopen(req) as r:
r.read()
def _state_file_path() -> Path:
workspace = os.environ.get("WORKSPACE_BASE", "")
if workspace:
root = Path(workspace).resolve().parent.parent
else:
root = Path.home() / ".openhands" / "workspaces"
state_dir = root / "automation-state"
state_dir.mkdir(parents=True, exist_ok=True)
payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))
automation_id = payload.get("automation_id", "default")
return state_dir / f"my_poller_{automation_id}.json"
def _default_state() -> dict:
return {"version": 1, "last_poll": None}
def load_state() -> dict:
if kv_available():
data = kv_get(_STATE_KEY)
if data is not None:
print("State loaded from KV store")
return data
return _default_state()
path = _state_file_path()
if path.exists():
try:
return json.loads(path.read_text())
except Exception as exc:
print(f"Warning: state file unreadable ({exc}); starting fresh")
return _default_state()
def save_state(state: dict) -> None:
if kv_available():
kv_set(_STATE_KEY, state)
print("State saved to KV store")
return
path = _state_file_path()
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(state, indent=2))
tmp.replace(path)
print(f"State saved to {path}")
```
> **Why a single document?** The KV store uses a single-document model under the hood (all keys for an automation share one encrypted row). Storing your entire state under a single key like `"state"` is the most efficient pattern — it avoids multiple round-trips and ensures atomic reads and writes.
---
## Environment Variables

@@ -455,2 +619,4 @@

| `AUTOMATION_EVENT_PAYLOAD` | — | JSON with trigger context: `automation_id`, `automation_name`, `trigger` type, and (for webhook runs) the raw event payload |
| `AUTOMATION_API_URL` | — | Base URL of the automation service (e.g., `https://app.all-hands.dev/api/automation`). Used to reach the KV store API |
| `AUTOMATION_KV_TOKEN` | — | Bearer token for the KV store API. Present whenever the service has `AUTOMATION_KV_SECRET` configured. Check for this variable to detect KV availability |

@@ -457,0 +623,0 @@ > **Note:** The session API key has two names: `SESSION_API_KEY` (cloud) and `OH_SESSION_API_KEYS_0` (local/dev). Always read both — see the code examples above.

@@ -109,2 +109,4 @@ ---

**State persistence between runs** — polling automations that track a "last processed" timestamp or active conversation IDs must use the built-in KV store rather than local files. Local files are lost when a run ends on a cloud pod. The KV store is available when `AUTOMATION_KV_TOKEN` is injected into the run environment. See `references/custom-automation.md#state-persistence-kv-store` for ready-to-copy `kv_get` / `kv_set` / `load_state` / `save_state` helpers.
---

@@ -875,3 +877,3 @@

- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.
- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.
- **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations.

@@ -89,2 +89,3 @@ ---

- [Context Condenser](https://docs.openhands.dev/sdk/guides/context-condenser.md): Manage agent memory by condensing conversation history to save tokens.
- [Conversation Goals](https://docs.openhands.dev/sdk/guides/agent-server/conversation-goals.md): Add a resumable goal strategy to a normal agent-server conversation.
- [Conversation with Async](https://docs.openhands.dev/sdk/guides/convo-async.md): Use async/await for concurrent agent operations and non-blocking execution.

@@ -96,2 +97,3 @@ - [Creating Custom Agent](https://docs.openhands.dev/sdk/guides/agent-custom.md): Learn how to design specialized agents with custom tool sets

- [Custom Visualizer](https://docs.openhands.dev/sdk/guides/convo-custom-visualizer.md): Customize conversation visualization by creating custom visualizers or configuring the default visualizer.
- [Deferred Init (Warm-Pool)](https://docs.openhands.dev/sdk/guides/agent-server/deferred-init.md): Pre-warm agent-server pods before a user is matched, then activate them at runtime with POST /api/init.
- [Docker Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/docker-sandbox.md): Run agent server in isolated Docker containers for security and reproducibility.

@@ -103,2 +105,3 @@ - [Exception Handling](https://docs.openhands.dev/sdk/guides/llm-error-handling.md): Provider‑agnostic exceptions raised by the SDK and recommended patterns for handling them.

- [Getting Started](https://docs.openhands.dev/sdk/getting-started.md): Install the OpenHands SDK and build AI agents that write software.
- [Goal Completion Loop](https://docs.openhands.dev/sdk/guides/convo-goal.md): Drive a conversation toward a verifiable objective with a judge-driven, self-continuing completion loop.
- [GPT-5 Preset (ApplyPatchTool)](https://docs.openhands.dev/sdk/guides/llm-gpt5-preset.md): Use the GPT-5 preset to build an agent that swaps the standard FileEditorTool for ApplyPatchTool.

@@ -115,3 +118,3 @@ - [Hello World](https://docs.openhands.dev/sdk/guides/hello-world.md): The simplest possible OpenHands agent - configure an LLM, create an agent, and complete a task.

- [LLM Subscriptions](https://docs.openhands.dev/sdk/guides/llm-subscriptions.md): Use your ChatGPT Plus/Pro subscription to access Codex models without consuming API credits.
- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Run agents through a local HTTP server with RemoteConversation for client-server architecture.
- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Install and run an OpenHands Agent Server on your machine, then connect to it from the SDK.
- [Metrics Tracking](https://docs.openhands.dev/sdk/guides/metrics.md): Track token usage, costs, and latency metrics for your agents.

@@ -121,2 +124,3 @@ - [Model Context Protocol](https://docs.openhands.dev/sdk/guides/mcp.md): Model Context Protocol (MCP) enables dynamic tool integration from external servers. Agents can discover and use MCP-provided tools automatically.

- [Observability & Tracing](https://docs.openhands.dev/sdk/guides/observability.md): Enable OpenTelemetry tracing to monitor and debug your agent's execution with tools like Laminar, MLflow, Honeycomb, or any OTLP-compatible backend.
- [OpenAI-Compatible Endpoint](https://docs.openhands.dev/sdk/guides/agent-server/openai-gateway.md): Call an OpenHands agent-server through the OpenAI Chat Completions protocol.
- [OpenHands Cloud Workspace](https://docs.openhands.dev/sdk/guides/agent-server/cloud-workspace.md): Connect to OpenHands Cloud for fully managed sandbox environments with optional SaaS credential inheritance.

@@ -188,3 +192,2 @@ - [Overview](https://docs.openhands.dev/sdk/guides/agent-server/overview.md): Run agents on remote servers with isolated workspaces for production deployments.

- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)
- [`43_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/43_mixed_marketplace_skills)
- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)

@@ -200,2 +203,3 @@ - [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)

- [`53_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/53_client_defined_tools.py)
- [`54_goal_completion_loop.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/54_goal_completion_loop.py)

@@ -219,2 +223,3 @@ ### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)

- [`15_openai_compatible_gateway.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/15_openai_compatible_gateway.py)
- [`16_deferred_init.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/16_deferred_init.py)
- [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)

@@ -241,2 +246,3 @@ - [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)

- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)
- [`04_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/04_mixed_marketplace_skills)

@@ -86,5 +86,5 @@ # Slack Channel Monitor

while ignoring replies that do not contain the trigger phrase
5. Checks active conversations - posts the agent's final response back to
Slack when the conversation completes, then watches briefly for triggered
follow-up replies
5. Checks active conversations - posts the agent's final response with
Slack's `markdown_text` field so Markdown renders correctly, then watches
briefly for triggered follow-up replies

@@ -91,0 +91,0 @@ ## See Also

@@ -1,5 +0,5 @@

# State File Schema
# State Schema
The automation maintains a JSON state file that persists across polling runs.
This file is the source of truth for which conversations are active, which
The automation maintains a JSON state document that persists across polling runs.
This document is the source of truth for which conversations are active, which
timestamps have been processed, and which messages were posted by the bot.

@@ -9,4 +9,11 @@

## File Location
## Storage
**Primary (cloud):** The state is stored in the automation service's built-in KV
store under the key `"state"`. The KV store is available when `AUTOMATION_KV_TOKEN`
is injected into the run environment. Each automation has its own isolated namespace.
**Fallback (local/dev):** When the KV store is not available, the state is written
to a local JSON file at:
```

@@ -13,0 +20,0 @@ {WORKSPACE_BASE_ROOT}/automation-state/slack_poller_{automation_id}.json

@@ -43,7 +43,6 @@ """

# ── Debug logging to a persistent file ────────────────────────────────────────
# ── Debug logging to a per-run file ───────────────────────────────────────────
_DEBUG_LOG_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(
os.environ.get("WORKSPACE_BASE", "/tmp")))),
"automation-state", "slack_poller_debug.log",
os.environ.get("WORKSPACE_BASE", "/tmp"),
"slack_poller_debug.log",
)

@@ -202,10 +201,43 @@ os.makedirs(os.path.dirname(_DEBUG_LOG_PATH), exist_ok=True)

# ── State management ───────────────────────────────────────────────────────────
# ── State persistence (KV store with local-file fallback) ─────────────────────
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
_STATE_KEY = "state"
def _kv_available() -> bool:
return bool(_KV_TOKEN and _KV_BASE)
def _kv_get(key: str) -> dict | None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["value"]
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def _kv_set(key: str, value: dict) -> None:
req = urllib.request.Request(
f"{_KV_BASE}/v1/kv/{key}",
data=json.dumps(value).encode(),
headers={
"Authorization": f"Bearer {_KV_TOKEN}",
"Content-Type": "application/json",
},
method="PUT",
)
with urllib.request.urlopen(req) as r:
r.read()
def _state_file_path() -> str:
"""Derive a persistent storage path from WORKSPACE_BASE.
WORKSPACE_BASE = {root}/automation-runs/{run_id}
State lives two levels up at {root}/automation-state/.
"""
"""Derive a persistent storage path from WORKSPACE_BASE (file fallback only)."""
workspace_base = os.environ.get("WORKSPACE_BASE", "")

@@ -225,18 +257,38 @@ event_payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))

def load_state(path: str) -> dict:
if os.path.exists(path):
return json.load(open(path))
def _default_state() -> dict:
return {
"version": 1,
"bot_user_id": None,
"last_poll": {}, # channel_id → float timestamp string
"conversations": {}, # conv_key → ConversationRecord (see schema docs)
"bot_message_ts": [], # ts strings of messages posted by this bot
"processed_ts": [], # ts strings of messages already handled (dedup)
"last_poll": {},
"conversations": {},
"bot_message_ts": [],
"processed_ts": [],
}
def save_state(path: str, state: dict) -> None:
def load_state() -> dict:
if _kv_available():
data = _kv_get(_STATE_KEY)
if data is not None:
print("State loaded from KV store")
return data
return _default_state()
path = _state_file_path()
if os.path.exists(path):
try:
return json.load(open(path))
except Exception as exc:
print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
return _default_state()
def save_state(state: dict) -> None:
if _kv_available():
_kv_set(_STATE_KEY, state)
print("State saved to KV store")
return
path = _state_file_path()
with open(path, "w") as f:
json.dump(state, f, indent=2)
print(f"State saved to {path}")

@@ -317,3 +369,8 @@

"""Post a Slack message and return its timestamp."""
body: dict = {"channel": channel, "text": text}
body: dict = {
"channel": channel,
"markdown_text": text,
"unfurl_links": False,
"unfurl_media": False,
}
if thread_ts:

@@ -672,14 +729,17 @@ body["thread_ts"] = thread_ts

def _expire_inactive_thread_watches(active_convs: dict[str, dict], now: float) -> None:
for conv_key, rec in active_convs.items():
if rec.get("status") != "watching":
continue
watch_until = float(rec.get("watch_until") or 0)
if watch_until and now >= watch_until:
rec["status"] = "closed"
rec["closed_reason"] = "followup_watch_expired"
rec["closed_at"] = now
print(f" Follow-up watch expired for {conv_key}")
def _close_thread_watch(rec: dict, conv_key: str, now: float) -> None:
rec["status"] = "closed"
rec["closed_reason"] = "followup_watch_expired"
rec["closed_at"] = now
print(f" Follow-up watch expired for {conv_key}")
def _next_reply_poll_at(rec: dict, now: float, delay: int) -> float:
next_poll_at = now + delay
watch_until = float(rec.get("watch_until") or 0)
if rec.get("status") == "watching" and watch_until:
next_poll_at = min(next_poll_at, watch_until)
return next_poll_at
def _poll_due_thread_replies(

@@ -692,3 +752,2 @@ slack_token: str,

now = time.time()
_expire_inactive_thread_watches(active_convs, now)
due: list[tuple[float, str, dict]] = []

@@ -699,2 +758,5 @@ for conv_key, rec in active_convs.items():

next_poll = float(rec.get("next_reply_poll_at") or 0)
watch_until = float(rec.get("watch_until") or 0)
if rec.get("status") == "watching" and watch_until and now >= watch_until:
next_poll = min(next_poll or watch_until, watch_until)
if next_poll <= now:

@@ -708,2 +770,4 @@ due.append((next_poll, conv_key, rec))

oldest = rec.get("last_seen_reply_ts") or thread_ts
watch_until = float(rec.get("watch_until") or 0)
watch_expired = rec.get("status") == "watching" and watch_until and now >= watch_until
try:

@@ -721,7 +785,10 @@ replies = thread_replies(slack_token, cid, thread_ts, oldest)

pass
rec["next_reply_poll_at"] = now + retry_after
rec["reply_poll_backoff_seconds"] = min(
THREAD_REPLY_MAX_BACKOFF_SECONDS,
max(retry_after, int(rec.get("reply_poll_backoff_seconds") or THREAD_REPLY_INITIAL_BACKOFF_SECONDS)),
)
if watch_expired:
_close_thread_watch(rec, conv_key, now)
else:
rec["next_reply_poll_at"] = _next_reply_poll_at(rec, now, retry_after)
rec["reply_poll_backoff_seconds"] = min(
THREAD_REPLY_MAX_BACKOFF_SECONDS,
max(retry_after, int(rec.get("reply_poll_backoff_seconds") or THREAD_REPLY_INITIAL_BACKOFF_SECONDS)),
)
print(f" Warning: could not fetch replies for thread {thread_ts}: {exc}")

@@ -745,2 +812,6 @@ continue

print(f" {conv_key}: {len(triggered_replies)} triggered follow-up reply/replies")
elif watch_expired:
if human_replies:
print(f" {conv_key}: ignored {len(human_replies)} follow-up reply/replies without trigger")
_close_thread_watch(rec, conv_key, now)
else:

@@ -755,4 +826,5 @@ if human_replies:

rec["reply_poll_backoff_seconds"] = next_backoff
rec["next_reply_poll_at"] = now + next_backoff
print(f" {conv_key}: no follow-ups; next reply poll in {next_backoff}s")
rec["next_reply_poll_at"] = _next_reply_poll_at(rec, now, next_backoff)
next_delay = max(0, int(rec["next_reply_poll_at"] - now))
print(f" {conv_key}: no follow-ups; next reply poll in {next_delay}s")

@@ -965,4 +1037,3 @@ return reply_messages

"""Run one polling cycle. Returns the last conversation ID created, if any."""
state_path = _state_file_path()
state = load_state(state_path)
state = load_state()

@@ -1154,4 +1225,3 @@ agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")

state["conversations"] = active_convs
save_state(state_path, state)
print(f"State saved to {state_path}")
save_state(state)
return last_conversation_id

@@ -1158,0 +1228,0 @@

@@ -259,4 +259,5 @@ ---

final response via `/api/conversations/{id}/agent_final_response` and post
it to the Slack thread. Mark the record `watching` for five minutes so
triggered follow-up replies can continue the same conversation.
it to the Slack thread using Slack's `markdown_text` field so Markdown
formatting renders correctly. Mark the record `watching` for five minutes
so triggered follow-up replies can continue the same conversation.
7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary

@@ -263,0 +264,0 @@ races). If a conversation creation failed, pins `last_poll` further back to

@@ -43,6 +43,6 @@ import json

assert entry["description"]
assert entry["kind"] in {"mcp", "http"}
assert entry["iconBg"]
# iconBg/iconColor are optional UI styling hints (OAuth-only entries may
# ship without a bespoke icon background).
assert "iconBg" not in entry or entry["iconBg"]
assert entry["connectionOptions"]
assert entry["defaultConnectionOptionId"]
for option in entry["connectionOptions"]:

@@ -76,2 +76,20 @@ assert option["id"]

def test_remote_no_auth_mcp_entries_are_intentionally_public():
public_remote_mcp_ids = {"cloudflare-docs", "deepwiki", "huggingface"}
actual = set()
for entry in load_catalog_entries("integrations/catalog"):
for option in entry["connectionOptions"]:
transport = option.get("transport", {})
if (
option["provider"] == "mcp"
and option["auth"]["strategy"] == "none"
and transport.get("url", "").startswith("https://")
):
actual.add(entry["id"])
assert transport["kind"] == "shttp"
assert actual == public_remote_mcp_ids
def test_credential_fields_have_helper_text_and_link():

@@ -78,0 +96,0 @@ """All password fields must have helperText plus a link (either a helperLink field or a

import type { ReactNode } from "react";
export const INTEGRATION_FALLBACK_LOGO: ReactNode;
export const INTEGRATION_LOGOS: Record<string, ReactNode>;
export const INTEGRATION_LOGO_IDS: Set<string>;
import { createElement } from "react";
import {
BookOpen,
Bot,
Brain,
Clock,
Database,
Flame,
Folder,
GitBranch,
Globe,
ListTree,
MousePointerClick,
Search,
Sparkles,
Telescope,
TestTube,
} from "lucide-react";
import {
SiAirtable,
SiAtlassian,
SiBrave,
SiClickhouse,
SiCloudflare,
SiElevenlabs,
SiFigma,
SiGithub,
SiHuggingface,
SiKagi,
SiLinear,
SiMongodb,
SiNotion,
SiObsidian,
SiPaypal,
SiRedis,
SiResend,
SiSentry,
SiSlack,
SiStripe,
SiSupabase,
} from "react-icons/si";
const LOGO = "h-5 w-5";
const simpleIcon = (Icon) => createElement(Icon, { className: LOGO });
const lucideIcon = (Icon) =>
createElement(Icon, { className: LOGO, strokeWidth: 2.25 });
export const INTEGRATION_FALLBACK_LOGO = lucideIcon(Bot);
export const INTEGRATION_LOGOS = {
github: simpleIcon(SiGithub),
slack: simpleIcon(SiSlack),
tavily: createElement(Search, { className: LOGO, strokeWidth: 2.5 }),
linear: simpleIcon(SiLinear),
notion: simpleIcon(SiNotion),
atlassian: simpleIcon(SiAtlassian),
sentry: simpleIcon(SiSentry),
stripe: simpleIcon(SiStripe),
paypal: simpleIcon(SiPaypal),
"cloudflare-docs": simpleIcon(SiCloudflare),
"cloudflare-bindings": simpleIcon(SiCloudflare),
"cloudflare-observability": simpleIcon(SiCloudflare),
huggingface: simpleIcon(SiHuggingface),
deepwiki: simpleIcon(BookOpen),
git: lucideIcon(GitBranch),
"brave-search": simpleIcon(SiBrave),
exa: lucideIcon(Telescope),
firecrawl: lucideIcon(Flame),
apify: lucideIcon(Bot),
fetch: lucideIcon(Globe),
"browser-mcp": lucideIcon(MousePointerClick),
playwright: lucideIcon(TestTube),
supabase: simpleIcon(SiSupabase),
neon: lucideIcon(Database),
mongodb: simpleIcon(SiMongodb),
redis: simpleIcon(SiRedis),
filesystem: lucideIcon(Folder),
memory: lucideIcon(Brain),
"sequential-thinking": lucideIcon(ListTree),
time: lucideIcon(Clock),
everything: lucideIcon(Sparkles),
figma: simpleIcon(SiFigma),
airtable: simpleIcon(SiAirtable),
obsidian: simpleIcon(SiObsidian),
elevenlabs: simpleIcon(SiElevenlabs),
resend: simpleIcon(SiResend),
"cloudflare-builds": simpleIcon(SiCloudflare),
"cloudflare-browser-rendering": simpleIcon(SiCloudflare),
kagi: simpleIcon(SiKagi),
clickhouse: simpleIcon(SiClickhouse),
};
export const INTEGRATION_LOGO_IDS = new Set(Object.keys(INTEGRATION_LOGOS));
import { getOAuthProviderRegistrationDefaults } from "./oauth-provider-registration-defaults.js";
const provider = (popularityRank, option) => {
const registrationDefaults = getOAuthProviderRegistrationDefaults(option.slug);
return {
...option,
authStrategy: option.authStrategy ?? registrationDefaults?.authStrategy ?? "oauth2",
popularityRank,
registrationDefaults,
};
};
const oauthProviderCatalog = [
provider(1, {
slug: "github",
name: "GitHub",
description: "Source control, issues, pull requests, and developer workflows.",
categories: ["Engineering", "Source control"],
availability: "manual_token",
managedConnectorSlug: "github",
appUrl: "https://github.com",
docsUrl: "https://docs.github.com/apps/oauth-apps/building-oauth-apps",
notes: "Current managed connector works with bearer tokens today; GitHub OAuth is a strong candidate for a future one-click connect flow.",
}),
provider(2, {
slug: "google-docs",
name: "Google Docs",
description: "Docs authoring and Google Workspace document automation.",
categories: ["Documents", "Knowledge base"],
availability: "manual_token",
managedConnectorSlug: "google-docs",
appUrl: "https://workspace.google.com/products/docs/",
docsUrl: "https://developers.google.com/identity/protocols/oauth2",
notes: "Current managed connector accepts a Google access token manually; a full OAuth connect flow can remove token copy/paste.",
}),
provider(3, {
slug: "slack",
name: "Slack",
description: "Channels, messaging, workflows, canvases, and operational collaboration.",
categories: ["Communication", "Operations"],
availability: "oauth_ready",
managedConnectorSlug: "slack",
appUrl: "https://slack.com",
docsUrl: "https://docs.slack.dev/ai/slack-mcp-server",
notes: "Uses Slack's official hosted MCP server with confidential OAuth user-token auth.",
}),
provider(4, {
slug: "notion",
name: "Notion",
description: "Workspace search, pages, databases, and knowledge management.",
categories: ["Knowledge base", "Documentation"],
availability: "oauth_ready",
managedConnectorSlug: "notion",
appUrl: "https://www.notion.so",
docsUrl: "https://developers.notion.com/docs/authorization",
notes: "",
}),
provider(5, {
slug: "figma",
name: "Figma",
description: "Design files, nodes, and developer handoff automation.",
categories: ["Design", "Frontend"],
availability: "oauth_ready",
managedConnectorSlug: "figma",
appUrl: "https://www.figma.com",
docsUrl: "https://www.figma.com/developers/api#oauth2",
notes: "",
}),
provider(6, {
slug: "google-drive",
name: "Google Drive",
description: "File search, metadata, folders, and document discovery.",
categories: ["Storage", "Documents"],
availability: "planned",
appUrl: "https://workspace.google.com/products/drive/",
docsUrl: "https://developers.google.com/identity/protocols/oauth2",
notes: "Natural expansion of the Google Workspace surface beyond Google Docs.",
}),
provider(7, {
slug: "google-sheets",
name: "Google Sheets",
description: "Spreadsheet reads, writes, formulas, and reporting workflows.",
categories: ["Spreadsheets", "Analytics"],
availability: "planned",
appUrl: "https://workspace.google.com/products/sheets/",
docsUrl: "https://developers.google.com/identity/protocols/oauth2",
notes: "High-value automation surface for agent-driven reporting and structured edits.",
}),
provider(8, {
slug: "gmail",
name: "Gmail",
description: "Mailbox search, drafting, sending, and thread triage.",
categories: ["Communication", "Email"],
availability: "planned",
appUrl: "https://workspace.google.com/products/gmail/",
docsUrl: "https://developers.google.com/identity/protocols/oauth2",
notes: "Popular agent workflow target for inbox triage and drafting.",
}),
provider(9, {
slug: "google-calendar",
name: "Google Calendar",
description: "Calendar search, event scheduling, and meeting coordination.",
categories: ["Calendar", "Scheduling"],
availability: "planned",
appUrl: "https://workspace.google.com/products/calendar/",
docsUrl: "https://developers.google.com/identity/protocols/oauth2",
notes: "Strong personal productivity use case with mature OAuth flows.",
}),
provider(10, {
slug: "jira",
name: "Jira",
description: "Issue tracking, sprint workflows, and engineering program management.",
categories: ["Project management", "Engineering"],
availability: "planned",
appUrl: "https://www.atlassian.com/software/jira",
docsUrl: "https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/",
notes: "Very common MCP target for software teams and ticket automation.",
}),
provider(11, {
slug: "confluence",
name: "Confluence",
description: "Wiki pages, spaces, and internal documentation search.",
categories: ["Documentation", "Knowledge base"],
availability: "planned",
appUrl: "https://www.atlassian.com/software/confluence",
docsUrl: "https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/",
notes: "Natural companion to Jira for software teams.",
}),
provider(12, {
slug: "linear",
name: "Linear",
description: "Issue tracking, roadmap planning, and product engineering workflows.",
categories: ["Project management", "Engineering"],
availability: "planned",
appUrl: "https://linear.app",
docsUrl: "https://linear.app/developers/oauth-authentication",
notes: "Popular among modern product engineering teams and already useful to this repo's users.",
}),
provider(13, {
slug: "asana",
name: "Asana",
description: "Task management, projects, and work tracking.",
categories: ["Project management", "Operations"],
availability: "planned",
appUrl: "https://asana.com",
docsUrl: "https://developers.asana.com/docs/oauth",
notes: "Broad business adoption and straightforward OAuth app model.",
}),
provider(14, {
slug: "trello",
name: "Trello",
description: "Boards, cards, checklists, and lightweight project planning.",
categories: ["Project management", "Operations"],
availability: "planned",
appUrl: "https://trello.com",
docsUrl: "https://developer.atlassian.com/cloud/trello/guides/rest-api/authorization/",
notes: "Useful for SMB and cross-functional task boards, but Trello's public API auth is a special-case flow rather than a straightforward generic OAuth2 connector.",
}),
provider(15, {
slug: "clickup",
name: "ClickUp",
description: "Tasks, docs, goals, and workflow automation.",
categories: ["Project management", "Operations"],
availability: "planned",
appUrl: "https://clickup.com",
docsUrl: "https://clickup.com/api/developer-portal/authentication",
notes: "Large install base and broad operations coverage.",
}),
provider(16, {
slug: "monday",
name: "Monday.com",
description: "Boards, automations, and work management across teams.",
categories: ["Project management", "Operations"],
availability: "planned",
appUrl: "https://monday.com",
docsUrl: "https://developer.monday.com/apps/docs/oauth",
notes: "High-demand operations platform with rich board APIs.",
}),
provider(17, {
slug: "airtable",
name: "Airtable",
description: "Bases, records, linked data, and workflow automation.",
categories: ["Database", "Operations"],
availability: "planned",
appUrl: "https://airtable.com",
docsUrl: "https://airtable.com/developers/web/api/oauth-reference",
notes: "Very common internal-tools and operations automation surface.",
}),
provider(18, {
slug: "dropbox",
name: "Dropbox",
description: "Cloud files, folders, content access, and sharing.",
categories: ["Storage", "Documents"],
availability: "planned",
appUrl: "https://www.dropbox.com",
docsUrl: "https://developers.dropbox.com/oauth-guide",
notes: "Popular file automation target with mature OAuth support.",
}),
provider(19, {
slug: "box",
name: "Box",
description: "Enterprise file storage, metadata, and collaboration.",
categories: ["Storage", "Enterprise"],
availability: "planned",
appUrl: "https://www.box.com",
docsUrl: "https://developer.box.com/guides/authentication/oauth2/",
notes: "Strong enterprise document-management footprint.",
}),
provider(20, {
slug: "microsoft-outlook",
name: "Microsoft Outlook",
description: "Mail, calendar, contacts, and meeting workflows through Microsoft Graph.",
categories: ["Email", "Calendar"],
availability: "planned",
appUrl: "https://www.microsoft.com/microsoft-365/outlook/email-and-calendar-software-microsoft-outlook",
docsUrl: "https://learn.microsoft.com/graph/auth-v2-user",
notes: "High-value Microsoft Graph integration surface for enterprise users.",
}),
provider(21, {
slug: "microsoft-teams",
name: "Microsoft Teams",
description: "Chats, channels, meetings, and collaboration via Microsoft Graph.",
categories: ["Communication", "Enterprise"],
availability: "planned",
appUrl: "https://www.microsoft.com/microsoft-teams/group-chat-software",
docsUrl: "https://learn.microsoft.com/graph/auth-v2-user",
notes: "Common enterprise chat target alongside Outlook.",
}),
provider(22, {
slug: "onedrive",
name: "OneDrive",
description: "Cloud file access and document workflows through Microsoft Graph.",
categories: ["Storage", "Documents"],
availability: "planned",
appUrl: "https://www.microsoft.com/microsoft-365/onedrive/online-cloud-storage",
docsUrl: "https://learn.microsoft.com/graph/auth-v2-user",
notes: "Useful for enterprise file automation and search.",
}),
provider(23, {
slug: "sharepoint",
name: "SharePoint",
description: "Sites, document libraries, lists, and intranet content.",
categories: ["Knowledge base", "Enterprise"],
availability: "planned",
appUrl: "https://www.microsoft.com/microsoft-365/sharepoint/collaboration",
docsUrl: "https://learn.microsoft.com/graph/auth-v2-user",
notes: "Frequently requested for enterprise knowledge retrieval.",
}),
provider(24, {
slug: "salesforce",
name: "Salesforce",
description: "CRM records, accounts, opportunities, and sales operations.",
categories: ["CRM", "Sales"],
availability: "planned",
appUrl: "https://www.salesforce.com",
docsUrl: "https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm&type=5",
notes: "Very common enterprise CRM and agent-assist target.",
}),
provider(25, {
slug: "hubspot",
name: "HubSpot",
description: "CRM, marketing, tickets, and customer lifecycle workflows.",
categories: ["CRM", "Marketing"],
availability: "oauth_ready",
managedConnectorSlug: "hubspot",
appUrl: "https://www.hubspot.com",
docsUrl:
"https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server",
notes: "Uses HubSpot's official remote MCP server plus MCP auth apps with PKCE.",
}),
provider(26, {
slug: "zendesk",
name: "Zendesk",
description: "Support tickets, customers, agents, and help desk operations.",
categories: ["Support", "Operations"],
availability: "planned",
appUrl: "https://www.zendesk.com",
docsUrl: "https://developer.zendesk.com/documentation/apps/getting-started/oauth/",
notes: "Strong support automation use case for agents.",
}),
provider(27, {
slug: "intercom",
name: "Intercom",
description: "Customer support, conversations, inboxes, and CRM context.",
categories: ["Support", "CRM"],
availability: "planned",
appUrl: "https://www.intercom.com",
docsUrl: "https://developers.intercom.com/building-apps/docs/authentication-types",
notes: "Useful for customer-facing assistant workflows.",
}),
provider(28, {
slug: "stripe",
name: "Stripe",
description: "Payments, customers, invoices, and subscription operations.",
categories: ["Payments", "Finance"],
availability: "planned",
appUrl: "https://stripe.com",
docsUrl: "https://docs.stripe.com/connect/oauth-reference",
notes: "High-value fintech automation target with mature OAuth patterns.",
}),
provider(29, {
slug: "shopify",
name: "Shopify",
description: "Storefronts, products, orders, and ecommerce operations.",
categories: ["Ecommerce", "Operations"],
availability: "planned",
appUrl: "https://www.shopify.com",
docsUrl: "https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant",
notes: "A top ecommerce platform and strong MCP candidate.",
}),
provider(30, {
slug: "discord",
name: "Discord",
description: "Guilds, channels, messages, and community operations.",
categories: ["Communication", "Community"],
availability: "planned",
appUrl: "https://discord.com",
docsUrl: "https://discord.com/developers/docs/topics/oauth2",
notes: "Useful for community automation and bot-assisted workflows.",
}),
provider(31, {
slug: "zoom",
name: "Zoom",
description: "Meetings, recordings, webinars, and scheduling operations.",
categories: ["Meetings", "Calendar"],
availability: "planned",
appUrl: "https://zoom.us",
docsUrl: "https://developers.zoom.us/docs/integrations/oauth/",
notes: "Frequently requested for scheduling and meeting summaries.",
}),
provider(32, {
slug: "webflow",
name: "Webflow",
description: "CMS items, sites, and web publishing workflows.",
categories: ["CMS", "Marketing"],
availability: "oauth_ready",
managedConnectorSlug: "webflow",
appUrl: "https://webflow.com",
docsUrl: "https://developers.webflow.com/mcp/reference/getting-started",
notes:
"Uses Webflow's official hosted MCP server and deployment-scoped MCP OAuth registration so each user can authorize their own sites and workspaces.",
}),
provider(33, {
slug: "miro",
name: "Miro",
description: "Whiteboards, diagrams, notes, and workshop collaboration.",
categories: ["Whiteboard", "Collaboration"],
availability: "planned",
appUrl: "https://miro.com",
docsUrl: "https://developers.miro.com/docs/rest-api-build-your-first-oauth-app",
notes: "Strong visual collaboration target for design and product teams.",
}),
provider(34, {
slug: "canva",
name: "Canva",
description: "Design content, brand assets, and marketing collateral workflows.",
categories: ["Design", "Marketing"],
availability: "planned",
appUrl: "https://www.canva.com",
docsUrl: "https://www.canva.dev/docs/connect/oauth/",
notes: "Broad creator and marketing adoption with OAuth-based apps.",
}),
provider(35, {
slug: "datadog",
name: "Datadog",
description: "Logs, metrics, monitors, incidents, and observability workflows.",
categories: ["Observability", "Operations"],
availability: "oauth_ready",
managedConnectorSlug: "datadog",
appUrl: "https://www.datadoghq.com",
docsUrl: "https://docs.datadoghq.com/bits_ai/mcp_server/setup/",
notes:
"Uses Datadog's official hosted MCP server. The default registration targets the US1 endpoint and can be edited for other Datadog sites.",
}),
provider(36, {
slug: "sentry",
name: "Sentry",
description: "Errors, releases, issue ownership, and incident investigation.",
categories: ["Observability", "Engineering"],
availability: "planned",
appUrl: "https://sentry.io",
docsUrl: "https://docs.sentry.io/api/guides/oauth/",
notes: "Natural target for engineering support agents.",
}),
provider(37, {
slug: "posthog",
name: "PostHog",
description: "Product analytics, feature flags, and session insights.",
categories: ["Analytics", "Product"],
availability: "planned",
appUrl: "https://posthog.com",
docsUrl: "https://posthog.com/docs/apps/build/oauth",
notes: "Relevant for product analytics and experimentation workflows.",
}),
provider(38, {
slug: "supabase",
name: "Supabase",
description: "Projects, databases, auth, and storage administration.",
categories: ["Database", "Developer tools"],
availability: "planned",
appUrl: "https://supabase.com",
docsUrl: "https://supabase.com/docs/guides/integrations/build-a-supabase-oauth-integration",
notes: "Popular developer platform with broad automation value and a documented management-API OAuth integration flow.",
}),
provider(39, {
slug: "vercel",
name: "Vercel",
description: "Deployments, projects, domains, and preview environment workflows.",
categories: ["Developer tools", "Deployment"],
availability: "planned",
appUrl: "https://vercel.com",
docsUrl: "https://vercel.com/docs/rest-api#oauth-apps",
notes: "Especially relevant to this repo's deployment model.",
}),
provider(40, {
slug: "netlify",
name: "Netlify",
description: "Sites, builds, deploy previews, and web operations.",
categories: ["Deployment", "Developer tools"],
availability: "planned",
appUrl: "https://www.netlify.com",
docsUrl: "https://docs.netlify.com/api/get-started/#oauth-applications",
notes: "Common alternative hosting/deployment automation target.",
}),
provider(41, {
slug: "plaid",
name: "Plaid",
description: "Financial account connectivity and transaction data flows.",
categories: ["Finance", "Payments"],
availability: "planned",
appUrl: "https://plaid.com",
docsUrl: "https://plaid.com/docs/auth/oauth/",
notes: "Strong fintech workflow candidate, although much of Plaid's OAuth guidance is a special-case token exchange rather than a generic end-user SaaS OAuth connector.",
}),
provider(42, {
slug: "okta",
name: "Okta",
description: "Identity, users, applications, and access administration.",
categories: ["Identity", "Enterprise"],
availability: "planned",
appUrl: "https://www.okta.com",
docsUrl: "https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/",
notes: "High-value admin and security automation surface.",
}),
provider(43, {
slug: "servicenow",
name: "ServiceNow",
description: "Tickets, incidents, service catalogs, and enterprise workflows.",
categories: ["ITSM", "Enterprise"],
availability: "planned",
appUrl: "https://www.servicenow.com",
docsUrl: "https://www.servicenow.com/docs/bundle/xanadu-platform-security/page/administer/security/concept/oauth-concept.html",
notes: "Strong enterprise IT operations use case.",
}),
provider(44, {
slug: "freshdesk",
name: "Freshdesk",
description: "Support tickets, customer records, and help desk workflows.",
categories: ["Support", "Operations"],
availability: "planned",
appUrl: "https://www.freshworks.com/freshdesk/",
docsUrl: "https://developers.freshdesk.com/api/#authentication",
notes: "Popular support platform for SMB and mid-market teams.",
}),
provider(45, {
slug: "pipedrive",
name: "Pipedrive",
description: "Deals, contacts, and sales pipeline management.",
categories: ["CRM", "Sales"],
availability: "planned",
appUrl: "https://www.pipedrive.com",
docsUrl: "https://pipedrive.readme.io/docs/marketplace-oauth-authorization",
notes: "Common CRM choice with solid OAuth story.",
}),
provider(46, {
slug: "mailchimp",
name: "Mailchimp",
description: "Campaigns, audiences, automations, and email marketing.",
categories: ["Marketing", "Email"],
availability: "planned",
appUrl: "https://mailchimp.com",
docsUrl: "https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/",
notes: "Popular marketing automation target.",
}),
provider(47, {
slug: "quickbooks",
name: "QuickBooks",
description: "Accounting, invoices, customers, and financial operations.",
categories: ["Finance", "Accounting"],
availability: "planned",
appUrl: "https://quickbooks.intuit.com",
docsUrl: "https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0",
notes: "Frequent finance automation request with OAuth-based apps.",
}),
provider(48, {
slug: "xero",
name: "Xero",
description: "Accounting, contacts, invoices, and bookkeeping workflows.",
categories: ["Finance", "Accounting"],
availability: "planned",
appUrl: "https://www.xero.com",
docsUrl: "https://developer.xero.com/documentation/guides/oauth2/overview/",
notes: "Important accounting platform in many regions.",
}),
provider(49, {
slug: "gitlab",
name: "GitLab",
description: "Repositories, issues, merge requests, pipelines, and DevSecOps.",
categories: ["Engineering", "Source control"],
availability: "planned",
appUrl: "https://about.gitlab.com",
docsUrl: "https://docs.gitlab.com/integration/oauth_provider/",
notes: "Major Git hosting platform and natural expansion beyond GitHub.",
}),
provider(50, {
slug: "bitbucket",
name: "Bitbucket",
description: "Repositories, pull requests, and Atlassian engineering workflows.",
categories: ["Engineering", "Source control"],
availability: "planned",
appUrl: "https://bitbucket.org",
docsUrl: "https://developer.atlassian.com/cloud/bitbucket/oauth-2/",
notes: "Popular in Atlassian-centric teams and complements Jira/Confluence.",
}),
provider(51, {
slug: "elevenlabs",
name: "ElevenLabs",
description: "Text to speech, voices, transcription, audio generation, and agents.",
categories: ["Audio", "AI"],
authStrategy: "api_key",
availability: "manual_token",
managedConnectorSlug: "elevenlabs",
appUrl: "https://elevenlabs.io",
docsUrl: "https://elevenlabs.io/docs/api-reference/introduction",
notes:
"Official API-key connector backed by ElevenLabs' published OpenAPI spec.",
}),
provider(52, {
slug: "ordinal",
name: "Ordinal",
description: "Social media posts, profiles, analytics, labels, approvals, and engagements.",
categories: ["Social media", "Marketing"],
authStrategy: "bearer",
availability: "manual_token",
managedConnectorSlug: "ordinal",
appUrl: "https://app.tryordinal.com",
docsUrl: "https://docs.tryordinal.com/api/mcp",
notes:
"Official MCP server backed by Ordinal's API with API-key (Bearer) auth.",
}),
];
export const listOAuthProviderCatalog = () => oauthProviderCatalog;
const googleAuthorizationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
const googleTokenUrl = "https://oauth2.googleapis.com/token";
const microsoftAuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
const microsoftTokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
const atlassianAuthorizationUrl = "https://auth.atlassian.com/authorize";
const atlassianTokenUrl = "https://auth.atlassian.com/oauth/token";
const googleWorkspacePreset = (
apiBaseUrl,
scopes,
toolName,
toolDescription,
requestPath,
requestMethod = "GET",
) => ({
apiBaseUrl,
authorizationUrl: googleAuthorizationUrl,
tokenUrl: googleTokenUrl,
scopes,
toolName,
toolDescription,
requestMethod,
requestPath,
});
const microsoftGraphPreset = (
scopes,
toolName,
toolDescription,
requestPath,
) => ({
apiBaseUrl: "https://graph.microsoft.com/v1.0",
authorizationUrl: microsoftAuthorizationUrl,
tokenUrl: microsoftTokenUrl,
scopes,
toolName,
toolDescription,
requestMethod: "GET",
requestPath,
});
const atlassianPreset = (
apiBaseUrl,
scopes,
toolName,
toolDescription,
requestPath,
) => ({
apiBaseUrl,
authorizationUrl: atlassianAuthorizationUrl,
tokenUrl: atlassianTokenUrl,
scopes,
toolName,
toolDescription,
requestMethod: "GET",
requestPath,
});
export const hubspotMcpServerUrl = "https://mcp.hubspot.com";
export const hubspotMcpAuthorizationUrl = `${hubspotMcpServerUrl}/oauth/authorize/user`;
export const hubspotMcpTokenUrl = `${hubspotMcpServerUrl}/oauth/v3/token`;
const officialManagedMcpServerUrls = {
github: "https://api.githubcopilot.com/mcp/",
hubspot: hubspotMcpServerUrl,
slack: "https://mcp.slack.com/mcp",
webflow: "https://mcp.webflow.com/mcp",
datadog: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
ordinal: "https://app.tryordinal.com/api/mcp",
};
export const hubspotRequiredScopes = [
"oauth",
"crm.objects.contacts.read",
];
export const hubspotOptionalScopes = [
"crm.objects.contacts.write",
"crm.objects.companies.read",
"crm.objects.companies.write",
"crm.objects.deals.read",
"crm.objects.deals.write",
"tickets",
"crm.objects.owners.read",
"crm.schemas.contacts.read",
"crm.schemas.companies.read",
"crm.schemas.deals.read",
];
const registrationDefaults = {
github: {
apiBaseUrl: "https://api.github.com",
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
scopes: ["read:user", "repo"],
toolName: "get_authenticated_user",
toolDescription: "Fetch the authenticated GitHub user profile.",
requestMethod: "GET",
requestPath: "/user",
},
"google-docs": googleWorkspacePreset(
"https://docs.googleapis.com",
["https://www.googleapis.com/auth/documents.readonly"],
"get_document",
"Fetch a Google Docs document by ID.",
"/v1/documents/{documentId}",
),
slack: {
provider: "mcp",
authorizationUrl: "https://slack.com/oauth/v2_user/authorize",
tokenUrl: "https://slack.com/api/oauth.v2.user.access",
scopes: [
"search:read.public",
"search:read.private",
"search:read.mpim",
"search:read.im",
"search:read.files",
"search:read.users",
"chat:write",
"channels:history",
"groups:history",
"mpim:history",
"im:history",
"canvases:read",
"canvases:write",
"users:read",
"users:read.email",
],
pkce: true,
clientAuthentication: "body",
},
notion: {
provider: "http",
apiBaseUrl: "https://api.notion.com",
openApiUrl: "https://developers.notion.com/openapi.json",
authorizationUrl: "https://api.notion.com/v1/oauth/authorize",
tokenUrl: "https://api.notion.com/v1/oauth/token",
scopes: [],
toolName: "post_search",
toolDescription: "Search pages and databases in the connected Notion workspace.",
requestMethod: "POST",
requestPath: "/v1/search",
},
figma: {
apiBaseUrl: "https://api.figma.com",
authorizationUrl: "https://www.figma.com/oauth",
tokenUrl: "https://api.figma.com/v1/oauth/token",
scopes: [
"current_user:read",
"file_content:read",
"file_metadata:read",
"projects:read",
],
toolName: "get_file",
toolDescription: "Fetch a Figma file by key.",
requestMethod: "GET",
requestPath: "/v1/files/{fileKey}",
},
elevenlabs: {
provider: "http",
authModes: ["api_key"],
authStrategy: "api_key",
credentialLabel: "ElevenLabs API key",
credentialPlaceholder: "Paste your ElevenLabs API key",
credentialHelp:
"Personal or workspace ElevenLabs API key sent in the xi-api-key header.",
apiKeyHeaderName: "xi-api-key",
apiBaseUrl: "https://api.elevenlabs.io",
openApiUrl: "https://api.elevenlabs.io/openapi.json",
},
"google-drive": googleWorkspacePreset(
"https://www.googleapis.com/drive/v3",
["https://www.googleapis.com/auth/drive.metadata.readonly"],
"list_files",
"List Drive files visible to the connected user.",
"/files",
),
"google-sheets": googleWorkspacePreset(
"https://sheets.googleapis.com",
["https://www.googleapis.com/auth/spreadsheets.readonly"],
"get_spreadsheet",
"Fetch spreadsheet metadata and sheets by ID.",
"/v4/spreadsheets/{spreadsheetId}",
),
gmail: googleWorkspacePreset(
"https://gmail.googleapis.com",
["https://www.googleapis.com/auth/gmail.readonly"],
"list_messages",
"List Gmail messages for the authenticated user.",
"/gmail/v1/users/me/messages",
),
"google-calendar": googleWorkspacePreset(
"https://www.googleapis.com/calendar/v3",
["https://www.googleapis.com/auth/calendar.readonly"],
"list_events",
"List events from the user's primary Google Calendar.",
"/calendars/primary/events",
),
jira: atlassianPreset(
"https://api.atlassian.com/ex/jira/{cloudId}",
["read:jira-work"],
"list_projects",
"List Jira projects available to the connected user.",
"/rest/api/3/project/search",
),
confluence: atlassianPreset(
"https://api.atlassian.com/ex/confluence/{cloudId}",
["read:page:confluence"],
"list_spaces",
"List Confluence spaces available to the connected user.",
"/wiki/api/v2/spaces",
),
linear: {
apiBaseUrl: "https://api.linear.app",
authorizationUrl: "https://linear.app/oauth/authorize",
tokenUrl: "https://api.linear.app/oauth/token",
scopes: ["read"],
toolName: "list_issues",
toolDescription: "Query issues from Linear via GraphQL.",
requestMethod: "POST",
requestPath: "/graphql",
},
asana: {
apiBaseUrl: "https://app.asana.com/api/1.0",
authorizationUrl: "https://app.asana.com/-/oauth_authorize",
tokenUrl: "https://app.asana.com/-/oauth_token",
scopes: ["tasks:read"],
toolName: "list_tasks",
toolDescription: "List tasks visible to the connected Asana user.",
requestMethod: "GET",
requestPath: "/tasks",
},
trello: {
apiBaseUrl: "https://api.trello.com/1",
toolName: "list_boards",
toolDescription: "List Trello boards visible to the authenticated member.",
requestMethod: "GET",
requestPath: "/members/me/boards",
},
clickup: {
apiBaseUrl: "https://api.clickup.com/api/v2",
authorizationUrl: "https://app.clickup.com/api",
tokenUrl: "https://api.clickup.com/api/v2/oauth/token",
scopes: [],
toolName: "list_workspaces",
toolDescription: "List ClickUp workspaces available to the connected user.",
requestMethod: "GET",
requestPath: "/team",
},
monday: {
apiBaseUrl: "https://api.monday.com/v2",
authorizationUrl: "https://auth.monday.com/oauth2/authorize",
tokenUrl: "https://auth.monday.com/oauth2/token",
scopes: ["boards:read"],
toolName: "list_boards",
toolDescription: "Query boards from Monday.com.",
requestMethod: "POST",
requestPath: "/",
},
airtable: {
apiBaseUrl: "https://api.airtable.com/v0",
authorizationUrl: "https://airtable.com/oauth2/v1/authorize",
tokenUrl: "https://airtable.com/oauth2/v1/token",
scopes: ["schema.bases:read", "data.records:read"],
toolName: "list_bases",
toolDescription: "List Airtable bases the connected user granted access to.",
requestMethod: "GET",
requestPath: "/meta/bases",
},
dropbox: {
apiBaseUrl: "https://api.dropboxapi.com/2",
authorizationUrl: "https://www.dropbox.com/oauth2/authorize",
tokenUrl: "https://api.dropboxapi.com/oauth2/token",
scopes: ["files.metadata.read"],
toolName: "list_root_folder",
toolDescription: "List entries in the root Dropbox folder.",
requestMethod: "POST",
requestPath: "/files/list_folder",
},
box: {
apiBaseUrl: "https://api.box.com/2.0",
authorizationUrl: "https://account.box.com/api/oauth2/authorize",
tokenUrl: "https://api.box.com/oauth2/token",
scopes: ["root_readonly", "item_read"],
toolName: "list_root_items",
toolDescription: "List files and folders in the Box root folder.",
requestMethod: "GET",
requestPath: "/folders/0/items",
},
"microsoft-outlook": microsoftGraphPreset(
["Mail.Read"],
"list_messages",
"List Outlook messages for the signed-in user.",
"/me/messages",
),
"microsoft-teams": microsoftGraphPreset(
["Team.ReadBasic.All"],
"list_teams",
"List Microsoft Teams joined by the signed-in user.",
"/me/joinedTeams",
),
onedrive: microsoftGraphPreset(
["Files.Read"],
"list_drive_items",
"List OneDrive items in the root folder.",
"/me/drive/root/children",
),
sharepoint: microsoftGraphPreset(
["Sites.Read.All"],
"get_root_site",
"Fetch the SharePoint root site through Microsoft Graph.",
"/sites/root",
),
salesforce: {
apiBaseUrl: "https://{instance}.salesforce.com/services/data/v60.0",
authorizationUrl: "https://login.salesforce.com/services/oauth2/authorize",
tokenUrl: "https://login.salesforce.com/services/oauth2/token",
scopes: ["api"],
toolName: "list_accounts",
toolDescription: "List Salesforce accounts from the connected org.",
requestMethod: "GET",
requestPath: "/sobjects/Account",
},
hubspot: {
provider: "mcp",
authorizationUrl: hubspotMcpAuthorizationUrl,
tokenUrl: hubspotMcpTokenUrl,
clientAuthentication: "body",
pkce: true,
scopes: [],
credentialHelp:
"Use the client ID and secret from a HubSpot MCP auth app (Development → MCP Auth Apps). Standard HubSpot OAuth apps and private apps will not authenticate with mcp.hubspot.com.",
},
zendesk: {
apiBaseUrl: "https://{subdomain}.zendesk.com/api/v2",
authorizationUrl: "https://{subdomain}.zendesk.com/oauth2/authorize",
tokenUrl: "https://{subdomain}.zendesk.com/oauth2/token",
scopes: ["tickets:read"],
toolName: "list_tickets",
toolDescription: "List Zendesk tickets for the connected account.",
requestMethod: "GET",
requestPath: "/tickets",
},
intercom: {
apiBaseUrl: "https://api.intercom.io",
authorizationUrl: "https://app.intercom.com/oauth",
tokenUrl: "https://api.intercom.io/auth/eagle/token",
scopes: ["read_users", "read_conversations"],
toolName: "list_contacts",
toolDescription: "List Intercom contacts for the connected workspace.",
requestMethod: "GET",
requestPath: "/contacts",
},
stripe: {
apiBaseUrl: "https://api.stripe.com/v1",
authorizationUrl: "https://connect.stripe.com/oauth/authorize",
tokenUrl: "https://connect.stripe.com/oauth/token",
scopes: ["read_only"],
toolName: "list_customers",
toolDescription: "List Stripe customers from the connected account.",
requestMethod: "GET",
requestPath: "/customers",
},
shopify: {
apiBaseUrl: "https://{shop}.myshopify.com/admin/api/2025-01",
authorizationUrl: "https://{shop}.myshopify.com/admin/oauth/authorize",
tokenUrl: "https://{shop}.myshopify.com/admin/oauth/access_token",
scopes: ["read_products"],
toolName: "list_products",
toolDescription: "List Shopify products for the connected store.",
requestMethod: "GET",
requestPath: "/products.json",
},
discord: {
apiBaseUrl: "https://discord.com/api/v10",
authorizationUrl: "https://discord.com/oauth2/authorize",
tokenUrl: "https://discord.com/api/oauth2/token",
scopes: ["identify", "guilds"],
toolName: "list_guilds",
toolDescription: "List Discord guilds available to the connected user.",
requestMethod: "GET",
requestPath: "/users/@me/guilds",
},
zoom: {
apiBaseUrl: "https://api.zoom.us/v2",
authorizationUrl: "https://zoom.us/oauth/authorize",
tokenUrl: "https://zoom.us/oauth/token",
scopes: ["meeting:read:user"],
toolName: "list_meetings",
toolDescription: "List Zoom meetings for the connected user.",
requestMethod: "GET",
requestPath: "/users/me/meetings",
},
webflow: {
provider: "mcp",
authorizationUrl: "https://mcp.webflow.com/oauth/authorize",
tokenUrl: "https://mcp.webflow.com/oauth/token",
registrationUrl: "https://mcp.webflow.com/oauth/register",
clientAuthentication: "none",
pkce: true,
scopes: [],
},
miro: {
apiBaseUrl: "https://api.miro.com/v2",
authorizationUrl: "https://miro.com/oauth/authorize",
tokenUrl: "https://api.miro.com/v1/oauth/token",
scopes: ["boards:read"],
toolName: "list_boards",
toolDescription: "List Miro boards available to the connected user.",
requestMethod: "GET",
requestPath: "/boards",
},
canva: {
apiBaseUrl: "https://api.canva.com/rest/v1",
tokenUrl: "https://api.canva.com/rest/v1/oauth/token",
scopes: [],
toolName: "list_designs",
toolDescription: "List Canva designs available to the connected user.",
requestMethod: "GET",
requestPath: "/designs",
},
datadog: {
provider: "mcp",
authorizationUrl: "https://app.datadoghq.com/oauth2/v1/authorize",
tokenUrl: "https://api.datadoghq.com/oauth2/v1/token",
scopes: ["dashboards_read", "monitors_read"],
},
sentry: {
apiBaseUrl: "https://sentry.io/api/0",
authorizationUrl: "https://sentry.io/oauth/authorize",
tokenUrl: "https://sentry.io/oauth/token",
scopes: ["project:read", "event:read", "org:read"],
toolName: "list_organizations",
toolDescription: "List Sentry organizations available to the connected user.",
requestMethod: "GET",
requestPath: "/organizations/",
},
posthog: {
apiBaseUrl: "https://us.posthog.com/api",
authorizationUrl: "https://us.posthog.com/oauth/authorize",
tokenUrl: "https://us.posthog.com/oauth/token",
scopes: ["project:read"],
toolName: "list_projects",
toolDescription: "List PostHog projects available to the connected user.",
requestMethod: "GET",
requestPath: "/projects/",
},
supabase: {
apiBaseUrl: "https://api.supabase.com/v1",
authorizationUrl: "https://api.supabase.com/v1/oauth/authorize",
tokenUrl: "https://api.supabase.com/v1/oauth/token",
scopes: [],
toolName: "list_projects",
toolDescription: "List Supabase projects available to the connected user.",
requestMethod: "GET",
requestPath: "/projects",
},
vercel: {
apiBaseUrl: "https://api.vercel.com",
authorizationUrl: "https://vercel.com/oauth/authorize",
tokenUrl: "https://api.vercel.com/v2/oauth/access_token",
scopes: ["project:read"],
toolName: "list_projects",
toolDescription: "List Vercel projects available to the connected user.",
requestMethod: "GET",
requestPath: "/v9/projects",
},
netlify: {
apiBaseUrl: "https://api.netlify.com/api/v1",
authorizationUrl: "https://app.netlify.com/authorize",
tokenUrl: "https://api.netlify.com/oauth/token",
scopes: ["sites:read"],
toolName: "list_sites",
toolDescription: "List Netlify sites available to the connected user.",
requestMethod: "GET",
requestPath: "/sites",
},
plaid: {
apiBaseUrl: "https://production.plaid.com",
toolName: "get_accounts",
toolDescription: "Fetch accounts for a connected Plaid item.",
requestMethod: "POST",
requestPath: "/accounts/get",
},
okta: {
apiBaseUrl: "https://{yourOktaDomain}/api/v1",
authorizationUrl: "https://{yourOktaDomain}/oauth2/v1/authorize",
tokenUrl: "https://{yourOktaDomain}/oauth2/v1/token",
scopes: ["okta.users.read"],
toolName: "list_users",
toolDescription: "List Okta users for the connected org.",
requestMethod: "GET",
requestPath: "/users",
},
servicenow: {
apiBaseUrl: "https://{instance}.service-now.com/api/now/v1",
authorizationUrl: "https://{instance}.service-now.com/oauth_auth.do",
tokenUrl: "https://{instance}.service-now.com/oauth_token.do",
scopes: ["useraccount", "table.read"],
toolName: "list_incidents",
toolDescription: "List ServiceNow incidents for the connected instance.",
requestMethod: "GET",
requestPath: "/table/incident",
},
freshdesk: {
apiBaseUrl: "https://{domain}.freshdesk.com/api/v2",
authorizationUrl: "https://{domain}.freshdesk.com/oauth/authorize",
tokenUrl: "https://{domain}.freshdesk.com/oauth/token",
scopes: ["tickets_view"],
toolName: "list_tickets",
toolDescription: "List Freshdesk tickets for the connected account.",
requestMethod: "GET",
requestPath: "/tickets",
},
pipedrive: {
apiBaseUrl: "https://api.pipedrive.com/v1",
authorizationUrl: "https://oauth.pipedrive.com/oauth/authorize",
tokenUrl: "https://oauth.pipedrive.com/oauth/token",
scopes: ["deals:read", "contacts:read"],
toolName: "list_persons",
toolDescription: "List Pipedrive people visible to the connected user.",
requestMethod: "GET",
requestPath: "/persons",
},
mailchimp: {
apiBaseUrl: "https://{dc}.api.mailchimp.com/3.0",
authorizationUrl: "https://login.mailchimp.com/oauth2/authorize",
tokenUrl: "https://login.mailchimp.com/oauth2/token",
scopes: ["audiences:read"],
toolName: "list_audiences",
toolDescription: "List Mailchimp audiences for the connected account.",
requestMethod: "GET",
requestPath: "/lists",
},
quickbooks: {
apiBaseUrl: "https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}",
authorizationUrl: "https://appcenter.intuit.com/connect/oauth2",
tokenUrl: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
scopes: ["com.intuit.quickbooks.accounting"],
toolName: "list_customers",
toolDescription: "Query QuickBooks customers for the connected company.",
requestMethod: "GET",
requestPath: "/query?query=SELECT%20*%20FROM%20Customer%20MAXRESULTS%2010",
},
xero: {
apiBaseUrl: "https://api.xero.com/api.xro/2.0",
authorizationUrl: "https://login.xero.com/identity/connect/authorize",
tokenUrl: "https://identity.xero.com/connect/token",
scopes: ["accounting.contacts.read"],
toolName: "list_contacts",
toolDescription: "List Xero contacts for the connected tenant.",
requestMethod: "GET",
requestPath: "/Contacts",
},
gitlab: {
apiBaseUrl: "https://gitlab.com/api/v4",
authorizationUrl: "https://gitlab.com/oauth/authorize",
tokenUrl: "https://gitlab.com/oauth/token",
scopes: ["read_api"],
toolName: "get_current_user",
toolDescription: "Fetch the authenticated GitLab user profile.",
requestMethod: "GET",
requestPath: "/user",
},
bitbucket: {
apiBaseUrl: "https://api.bitbucket.org/2.0",
authorizationUrl: "https://bitbucket.org/site/oauth2/authorize",
tokenUrl: "https://bitbucket.org/site/oauth2/access_token",
scopes: ["account", "repository"],
toolName: "get_current_user",
toolDescription: "Fetch the authenticated Bitbucket user profile.",
requestMethod: "GET",
requestPath: "/user",
},
ordinal: {
provider: "mcp",
authModes: ["bearer"],
authStrategy: "bearer",
credentialLabel: "Ordinal API key",
credentialPlaceholder: "Paste your Ordinal API key",
credentialHelp:
"API key from your Ordinal workspace settings, sent as a Bearer token in the Authorization header.",
},
};
export const getOAuthProviderRegistrationDefaults = (slug) => {
const defaults = registrationDefaults[slug];
if (!defaults) {
return undefined;
}
const officialMcpServerUrl = officialManagedMcpServerUrls[slug];
const provider =
defaults.provider ??
(officialMcpServerUrl
? "mcp"
: defaults.apiBaseUrl || defaults.openApiUrl
? "http"
: undefined);
return {
...defaults,
provider,
serverUrl:
provider === "mcp"
? defaults.serverUrl ?? officialMcpServerUrl
: defaults.serverUrl,
};
};
---
name: cobol-modernization
description: End-to-end COBOL to Java migration workflow. Handles build setup, mainframe dependency removal, and code migration with test validation.
license: MIT
compatibility: Requires GnuCOBOL, Java 11+, Maven/Gradle, Python 3.13 with uv
triggers:
- cobol modernization
- cobol to java
- cobol migration
- mainframe migration
---
End-to-end workflow for migrating COBOL codebases to Java.
## Overview
This plugin orchestrates a multi-phase COBOL modernization project:
1. **Build Setup** — Configure compilation for both COBOL and Java, create test fixtures
2. **Mainframe Planning** — Document transformations needed to remove mainframe dependencies
3. **Mainframe Removal** — Convert CICS/VSAM code to standard COBOL
4. **Java Migration** — Translate standardized COBOL to idiomatic Java
## Prerequisites
- GnuCOBOL compiler (`cobc`)
- Java 11+ with Maven or Gradle
- Python 3.13 with `uv`
- LLM API key (Anthropic or OpenAI)
## Quick Start
```bash
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
uv run python -m lc_sdk_examples.cobol_modernization --src-path /path/to/cobol/project
```
## Workflow Phases
### Phase 1: Build Setup
See [../build-setup/SKILL.md](../build-setup/SKILL.md)
Creates the foundation for the migration:
- COBOL compilation environment (GnuCOBOL)
- Java project structure (Maven/Gradle + JUnit 5)
- Test fixtures with golden outputs from COBOL execution
**Outputs:**
- `build_notes.md` — Build instructions
- `test-fixtures/` — Input/output test data
- `test_manifest.json` — Test case mapping
### Phase 2: Mainframe Planning
See [../mainframe-planning/SKILL.md](../mainframe-planning/SKILL.md)
Creates a transformation guide without modifying code:
- Maps CICS/VSAM constructs to standard COBOL equivalents
- Documents error handling replacements
- Identifies UI operations to stub
**Output:**
- `mainframe_dependency_removal_plan.md`
### Phase 3: Mainframe Removal
See [../mainfraime-removal/SKILL.md](../mainfraime-removal/SKILL.md)
Applies the transformation guide:
- Replaces EXEC CICS commands with file I/O
- Adds FILE STATUS checking
- Stubs BMS/screen operations
**Verification:**
- Code compiles with GnuCOBOL
- Runs with test fixtures
### Phase 4: Java Migration
See [../to-java-migration/SKILL.md](../to-java-migration/SKILL.md)
Translates to idiomatic Java:
- Proper Java conventions (not literal translations)
- JUnit tests using golden outputs
- COBOL references in comments
**Done when:**
- All code compiles
- All JUnit tests pass
- No TODOs or stubs remain
## Output Structure
```
your-project/
├── .lc-sdk/
│ ├── initial_batch_graph.json
│ ├── fixed_batch_graph.json
│ └── mainframe_dependency_removal_plan.md
├── test-fixtures/
│ ├── inputs/
│ └── expected_outputs/
├── test_manifest.json
├── src/main/java/
└── src/test/java/
```
## Troubleshooting
See [../../references/troubleshooting.md](../../references/troubleshooting.md) for common issues and solutions.
# CICS Transformation Examples
## UI/Terminal Operations
### Before (CICS)
```cobol
EXEC CICS SEND MAP('CUSTMAP') MAPSET('CUSTSET') END-EXEC
```
### After (Standard COBOL)
```cobol
DISPLAY "Customer Screen Output"
DISPLAY WS-CUSTOMER-NAME
DISPLAY WS-CUSTOMER-ADDRESS
```
---
## Error Handling
### Before (CICS with RESP/RESP2)
```cobol
EXEC CICS READ FILE('CUSTFILE')
INTO(WS-CUSTOMER-REC)
RIDFLD(WS-CUST-KEY)
RESP(WS-RESP)
RESP2(WS-RESP2)
END-EXEC
IF WS-RESP = DFHRESP(NOTFND)
PERFORM CUSTOMER-NOT-FOUND
END-IF
```
### After (Standard COBOL with FILE STATUS)
```cobol
READ CUSTFILE INTO WS-CUSTOMER-REC
KEY IS WS-CUST-KEY
IF FILE-STATUS NOT = '00'
IF FILE-STATUS = '23'
PERFORM CUSTOMER-NOT-FOUND
ELSE
PERFORM FILE-ERROR-HANDLER
END-IF
END-IF
```
---
name: mainframe-removal
description: Apply mainframe dependency transformations to COBOL code using a pre-generated transformation guide. Converts CICS/VSAM constructs to standard COBOL.
license: MIT
compatibility: Requires GnuCOBOL (cobc) for verification
triggers:
- remove mainframe
- cobol transformation
- cics removal
- standard cobol
---
Apply the transformations from a transformation guide to convert mainframe-specific COBOL to standard COBOL.
**Prerequisite**: A transformation guide must exist (see `cobol-mainframe-planning` skill).
## Transformation Requirements
### Data Operations
- Replace each CICS/VSAM construct with its standard COBOL equivalent per the plan
- Add FILE STATUS checks after EVERY file operation:
- Check for success (00) before proceeding
- Handle "not found" (23) distinctly from I/O errors (3x)
- Handle "file not exists" (35) at OPEN time
- Add explicit CLOSE statements in all code paths (including error paths)
### UI/Terminal Operations (BMS maps, SEND/RECEIVE)
- Replace with simple stubs or ACCEPT/DISPLAY statements
- Do NOT spend time replicating screen layouts
- Focus on preserving data flow, not UI fidelity
### Error Handling
- Replace CICS RESP/RESP2 checks with equivalent FILE STATUS logic
- Replace HANDLE CONDITION with explicit status checking after operations
- Ensure error paths don't leave files open
See [references/cics-transformation-examples.md](references/cics-transformation-examples.md) for before/after code examples.
## Verification
After transformation:
- Code MUST compile without errors
- Test with valid input → should execute core business logic
- Test with missing/invalid files → should fail gracefully, not crash
## Preserve
- All original business logic
- Data transformations and calculations
- Validation rules
## Checklist
- [ ] All EXEC CICS commands replaced
- [ ] FILE STATUS declared for all files
- [ ] FILE STATUS checked after every I/O operation
- [ ] CLOSE statements in all code paths
- [ ] Code compiles successfully
- [ ] Basic test execution passes
---
name: migration-scoring
description: Evaluate code migration quality with coverage, correctness, and style scoring. Generates executive reports with actionable recommendations.
license: MIT
compatibility: Requires completed migration with source and target code, Python 3.13 with uv
triggers:
- migration scoring
- migration quality
- migration evaluation
- score migration
---
Comprehensive quality evaluation for code migration projects.
## Overview
This plugin evaluates completed migrations through multiple lenses:
1. **Mapping** — Document source-to-target file relationships
2. **Quality Scoring** — Measure coverage and correctness
3. **Style Scoring** — Evaluate code quality and conventions
4. **Reporting** — Generate executive summary with recommendations
## Prerequisites
- Completed migration with both source and target code present
- Python 3.13 with `uv`
- LLM API key (Anthropic or OpenAI)
- Optional: Custom style rubric file
## Quick Start
```bash
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
uv run python -m lc_sdk_examples.migration_scoring \
--src-path /path/to/migration/project \
--rubric-path /path/to/style_rubric.txt
```
## Workflow Phases
### Phase 1: Migration Mapping
See [../migration-mapping/SKILL.md](../migration-mapping/SKILL.md)
Creates a source→target file mapping:
- Identifies which target files implement each source file
- Supports many-to-many relationships
- Flags unmigrated source files
**Output:** `migration_mapping.json`
```json
{
"CALC001.cbl": ["InvoiceCalculator.java", "TaxCalculator.java"],
"CUST002.cbl": ["CustomerService.java"]
}
```
### Phase 2: Quality Scoring
See [../score-quality/SKILL.md](../score-quality/SKILL.md)
Scores each source file on:
- **Coverage (1-5)**: How much functionality was migrated
- **Correctness (1-5)**: How accurately behavior was preserved
**Output:** `migration_score.json`
```json
{
"CALC001.cbl": {
"coverage": 4,
"correctness": 5,
"justification": "All calculation logic migrated..."
}
}
```
### Phase 3: Style Scoring
See [../score-style/SKILL.md](../score-style/SKILL.md)
Evaluates target code against style guidelines:
- Naming conventions
- Code organization
- Error handling
- Documentation
- Idiomaticity
**Output:** `style_score.json`
### Phase 4: Executive Report
See [../migration-report/SKILL.md](../migration-report/SKILL.md)
Generates a comprehensive report:
- Overall health assessment
- Score statistics and distribution
- Risk categorization (Green/Yellow/Red)
- Prioritized recommendations
**Output:** `final_report.md`
## Output Structure
```
your-project/
├── .lc-sdk/
│ ├── migration_mapping.json
│ ├── migration_score.json
│ ├── style_score.json
│ └── final_report.md
```
## Scoring Criteria
See [../score-quality/references/scoring-criteria.md](../score-quality/references/scoring-criteria.md) for the 1-5 scoring scales.
### Risk Categories
- **Green**: All scores ≥ 4
- **Yellow**: Any score 3-4
- **Red**: Any score < 3

Sorry, the diff of this file is too big to display