@openhands/extensions
Advanced tools
| 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() |
| 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/ |
| { | ||
| ".": "0.5.0" | ||
| ".": "0.6.0" | ||
| } |
+1
-1
@@ -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 @@ { |
+24
-60
@@ -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; |
+25
-203
@@ -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": [ |
+4
-7
| { | ||
| "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 |
+17
-3
| [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"] |
+31
-3
@@ -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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2160445
3.31%468
11.96%22341
5.96%200
16.28%1
-75%54
1.89%