diff --git a/apps/api/src/handlers/slack/helpers/event-normalization.test.ts b/apps/api/src/handlers/slack/helpers/event-normalization.test.ts index 086ad6fde8..1f81d4c8e8 100644 --- a/apps/api/src/handlers/slack/helpers/event-normalization.test.ts +++ b/apps/api/src/handlers/slack/helpers/event-normalization.test.ts @@ -74,12 +74,31 @@ describe('event-normalization', () => { ); }); - it('adds Cloudflare setup guidance to the active Slack agent context', () => { + it('adds Buildkite setup guidance to the active Slack agent context', () => { const event = { type: 'app_mention', channel: 'C123', user: 'U123', ts: '1712345678.000150', + text: '<@U_ROOMOTE> inspect https://buildkite.com/acme/pipelines/api', + } as SlackEvent; + + enrichSlackMessageEvent(event); + + expect(event.agentContext).toContain( + 'Slack integration setup recommendations:\n- Buildkite:', + ); + expect(event.text).toContain( + 'Slack integration setup recommendations:\n- Buildkite:', + ); + }); + + it('adds Cloudflare setup guidance to the active Slack agent context', () => { + const event = { + type: 'app_mention', + channel: 'C123', + user: 'U123', + ts: '1712345678.000175', text: '<@U_ROOMOTE> inspect https://dash.cloudflare.com/example/workers', } as SlackEvent; diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 658b35066e..030bcc6ae4 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -154,6 +154,7 @@ "integrations/roomote-mcp", "integrations/asana", "integrations/better-stack", + "integrations/buildkite", "integrations/braintrust", "integrations/cloudflare", "integrations/elevenlabs", diff --git a/apps/docs/integrations/buildkite.mdx b/apps/docs/integrations/buildkite.mdx new file mode 100644 index 0000000000..edd4638b25 --- /dev/null +++ b/apps/docs/integrations/buildkite.mdx @@ -0,0 +1,32 @@ +--- +title: Buildkite +description: Inspect Buildkite pipelines, builds, jobs, and tests from Roomote tasks. +icon: 'https://api.iconify.design/simple-icons:buildkite.svg?color=currentColor' +--- + +Connect Buildkite when tasks need CI pipeline, build, job, log, artifact, test, +cluster, agent, or queue context. + +## How setup works + +A deployment operator connects Buildkite once from **Settings > Integrations** +using OAuth. The built-in integration uses Buildkite's hosted +`/mcp/readonly` endpoint and requests its `read` scope. Buildkite's public OAuth +metadata supports dynamic client registration and PKCE, so self-hosted Roomote +deployments do not need a preconfigured Buildkite OAuth client. + +The organization picker shown during authorization is a convenience, not an +access-control boundary. The connection can see organizations available to the +authorizing account, subject to Buildkite's permissions. + +## Network restrictions + +If a Buildkite organization uses an API IP allowlist, add Buildkite's published +MCP egress addresses to that allowlist. The hosted MCP server calls the +Buildkite API from Buildkite infrastructure. + +## Verify the connection + +Start by listing pipelines or inspecting a recent failed build. This built-in +connection is read-only; use a separately configured custom MCP server if a +different endpoint or token-backed toolset is required. diff --git a/apps/docs/integrations/index.mdx b/apps/docs/integrations/index.mdx index 1eb8aac84b..55c75527f9 100644 --- a/apps/docs/integrations/index.mdx +++ b/apps/docs/integrations/index.mdx @@ -78,6 +78,7 @@ from [Personal Settings](/personal-settings). | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------ | | | Project and task context from Asana | Admin connection once | | | Monitoring and incident context | Admin connection once | +| | Read-only CI pipeline and build context | Admin connection once | | | Prompts, runs, and evaluation context | Enable first, then teammates link accounts | | | Cloud infrastructure and API operations | Admin connection once | | | Voice narration for feature-demo videos | Admin connection once | diff --git a/apps/web/src/components/settings/Integrations.tsx b/apps/web/src/components/settings/Integrations.tsx index ecc41cceff..e364fa2957 100644 --- a/apps/web/src/components/settings/Integrations.tsx +++ b/apps/web/src/components/settings/Integrations.tsx @@ -109,6 +109,8 @@ const DEEP_LINK_ENABLE_DESCRIPTIONS: Record = { 'Roomote will be able to inspect monitoring, incidents, and telemetry.', braintrust: 'Roomote will be able to inspect prompts, evaluations, and AI run history.', + buildkite: + 'Roomote will be able to inspect Buildkite pipelines, builds, jobs, logs, tests, artifacts, and agents through a read-only connection.', cloudflare: 'Roomote will be able to inspect Cloudflare resources and run explicitly requested API operations with the permissions granted during OAuth.', grafana: diff --git a/apps/web/src/components/system/custom/logos/brand-icon.tsx b/apps/web/src/components/system/custom/logos/brand-icon.tsx index 3011e5ea4d..12a4533a42 100644 --- a/apps/web/src/components/system/custom/logos/brand-icon.tsx +++ b/apps/web/src/components/system/custom/logos/brand-icon.tsx @@ -4,6 +4,7 @@ import { siAsana, siBetterstack, siBraintrust, + siBuildkite, siCloudflare, siElevenlabs, siDependabot, @@ -43,6 +44,7 @@ const SIMPLE_ICONS: Record = { asana: siAsana, betterstack: siBetterstack, braintrust: siBraintrust, + buildkite: siBuildkite, cloudflare: siCloudflare, elevenlabs: siElevenlabs, dependabot: siDependabot, diff --git a/apps/worker/src/mcp/roomote-mcp-server/integration-setup.ts b/apps/worker/src/mcp/roomote-mcp-server/integration-setup.ts index d60cd2e6e5..2823ced78e 100644 --- a/apps/worker/src/mcp/roomote-mcp-server/integration-setup.ts +++ b/apps/worker/src/mcp/roomote-mcp-server/integration-setup.ts @@ -53,6 +53,14 @@ Sentry uses the workspace MCP integration: Once connected, tasks can inspect Sentry issue and project context, and scheduled Sentry triage automation uses the same read-only MCP connection. +# Buildkite + +Buildkite uses OAuth: +1. A deployment operator enables Buildkite from Settings > Integrations. +2. That operator connects Buildkite once for the deployment via OAuth. + +Once connected, I can inspect pipelines, builds, jobs, logs, artifacts, tests, clusters, agents, and queues through Buildkite's read-only hosted MCP endpoint. Organizations that restrict API access by IP must allowlist Buildkite's published MCP egress addresses. Organization selection during OAuth is a convenience, not an access-control boundary. + # Cloudflare Cloudflare uses OAuth: diff --git a/packages/cloud-agents/src/server/mcp-self-setup/catalog.ts b/packages/cloud-agents/src/server/mcp-self-setup/catalog.ts index a89ca7a782..2819790f61 100644 --- a/packages/cloud-agents/src/server/mcp-self-setup/catalog.ts +++ b/packages/cloud-agents/src/server/mcp-self-setup/catalog.ts @@ -38,6 +38,13 @@ export const MCP_SETUP_INTEGRATION_METADATA: Record< string, SetupMcpIntegrationMetadata > = { + buildkite: { + capabilities: [ + 'Inspect Buildkite pipelines, builds, jobs, and logs', + 'Review artifacts, annotations, tests, agents, and queues', + 'Keep hosted MCP access read-only at the provider endpoint', + ], + }, cloudflare: { capabilities: [ 'Search the Cloudflare API for available operations', diff --git a/packages/slack/src/__tests__/forwarded-message-context.test.ts b/packages/slack/src/__tests__/forwarded-message-context.test.ts index d59b970a11..eba0ae7eaf 100644 --- a/packages/slack/src/__tests__/forwarded-message-context.test.ts +++ b/packages/slack/src/__tests__/forwarded-message-context.test.ts @@ -203,6 +203,19 @@ describe('forwarded-message-context', () => { ); }); + it('adds a setup recommendation for pasted Buildkite organization URLs', () => { + expect( + formatSlackMcpSetupRecommendationContext( + 'Can you inspect https://buildkite.com/acme/pipelines/api/builds/42?', + ), + ).toBe( + [ + 'Slack integration setup recommendations:', + '- Buildkite: if it is unavailable, offer to connect the built-in integration from /integrations.', + ].join('\n'), + ); + }); + it('adds a setup recommendation for pasted Cloudflare dashboard URLs', () => { expect( formatSlackMcpSetupRecommendationContext( @@ -216,7 +229,32 @@ describe('forwarded-message-context', () => { ); }); - it('adds the recommendation to active Slack agent context', () => { + it('adds the Buildkite recommendation to active Slack agent context', () => { + expect( + formatSlackAttachmentContext('Can you inspect this?', undefined, [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: '', + }, + }, + ]), + ).toContain('Slack integration setup recommendations:\n- Buildkite:'); + }); + + it('detects Buildkite URLs exposed only through attachment title links', () => { + expect( + formatSlackAttachmentContext('Can you inspect this?', [ + { + title: 'Build #42', + title_link: 'https://buildkite.com/acme/pipelines/api/builds/42', + }, + ]), + ).toContain('Slack integration setup recommendations:\n- Buildkite:'); + }); + + it('adds the Cloudflare recommendation to active Slack agent context', () => { expect( formatSlackAttachmentContext('Can you inspect this?', undefined, [ { diff --git a/packages/slack/src/forwarded-message-context.ts b/packages/slack/src/forwarded-message-context.ts index 74bd9bbc17..f6eecd64a5 100644 --- a/packages/slack/src/forwarded-message-context.ts +++ b/packages/slack/src/forwarded-message-context.ts @@ -1185,6 +1185,10 @@ export function formatSlackMcpSetupRecommendationContext( extractBlockLinks(blocks, links, seenKeys); for (const attachment of attachments ?? []) { if (isRecord(attachment)) { + const titleLink = getStringField(attachment, 'title_link'); + if (titleLink) { + appendUniqueSlackBlockLink(links, seenKeys, { url: titleLink }); + } extractBlockLinks(attachment.blocks, links, seenKeys); } } diff --git a/packages/slack/src/mcp-recommendations.ts b/packages/slack/src/mcp-recommendations.ts index f40ba5881a..f27376b93e 100644 --- a/packages/slack/src/mcp-recommendations.ts +++ b/packages/slack/src/mcp-recommendations.ts @@ -57,6 +57,8 @@ const SLACK_ENABLE_DESCRIPTIONS: Record = { 'Roomote will be able to inspect and manage shared email infrastructure.', braintrust: 'Roomote will be able to inspect prompts, evaluations, and AI run history.', + buildkite: + 'Roomote will be able to inspect Buildkite pipelines, builds, jobs, logs, tests, artifacts, and agents through a read-only connection.', cloudflare: 'Roomote will be able to inspect Cloudflare resources and run explicitly requested API operations with the connected permissions.', linear: diff --git a/packages/types/src/__tests__/mcp-oauth.test.ts b/packages/types/src/__tests__/mcp-oauth.test.ts index 37858f5442..8c7d1426d1 100644 --- a/packages/types/src/__tests__/mcp-oauth.test.ts +++ b/packages/types/src/__tests__/mcp-oauth.test.ts @@ -89,6 +89,21 @@ describe('monday.com OAuth', () => { }); }); +describe('Buildkite OAuth', () => { + it('uses the provider-enforced read-only MCP endpoint with DCR', () => { + expect(getMcpIntegration('buildkite')).toMatchObject({ + name: 'Buildkite', + url: 'https://mcp.buildkite.com/mcp/readonly', + connectionScope: 'deployment', + oauthResource: 'https://mcp.buildkite.com/mcp/readonly', + oauthScopes: ['read'], + oauthScopeMode: 'read-only', + }); + expect(getMcpIntegration('buildkite')?.oauthClientEnv).toBeUndefined(); + expect(getMcpIntegrationDefaultDisabledTools('buildkite')).toEqual([]); + }); +}); + describe('Cloudflare OAuth', () => { it('uses the hosted API MCP with a deployment-scoped DCR connection', () => { expect(getMcpIntegration('cloudflare')).toMatchObject({ diff --git a/packages/types/src/__tests__/mcp-service-detection.test.ts b/packages/types/src/__tests__/mcp-service-detection.test.ts index 6dda76d06d..b13f50eedb 100644 --- a/packages/types/src/__tests__/mcp-service-detection.test.ts +++ b/packages/types/src/__tests__/mcp-service-detection.test.ts @@ -4,6 +4,37 @@ import { } from '../mcp-service-detection'; describe('Slack MCP setup service detection', () => { + it('matches Buildkite organization URLs from plain and Slack-formatted text', () => { + expect( + matchSlackMcpSetupServiceUrl( + 'https://buildkite.com/acme/pipelines/api/builds/42', + )?.id, + ).toBe('buildkite'); + + expect( + findSlackMcpSetupServicesInText( + 'Check .', + ).map((service) => service.id), + ).toEqual(['buildkite']); + expect( + findSlackMcpSetupServicesInText( + 'Check https://buildkite.com/acme/pipelines/api!!!!', + ).map((service) => service.id), + ).toEqual(['buildkite']); + }); + + it('does not match Buildkite public, API, or MCP URLs', () => { + expect( + matchSlackMcpSetupServiceUrl('https://buildkite.com/docs/pipelines'), + ).toBeUndefined(); + expect( + matchSlackMcpSetupServiceUrl('https://api.buildkite.com/v2/builds'), + ).toBeUndefined(); + expect( + matchSlackMcpSetupServiceUrl('https://mcp.buildkite.com/mcp/readonly'), + ).toBeUndefined(); + }); + it('matches Cloudflare dashboard URLs from plain and Slack-formatted text', () => { expect( matchSlackMcpSetupServiceUrl( diff --git a/packages/types/src/mcp-oauth.ts b/packages/types/src/mcp-oauth.ts index 5182010bac..3a15f67c64 100644 --- a/packages/types/src/mcp-oauth.ts +++ b/packages/types/src/mcp-oauth.ts @@ -652,6 +652,19 @@ export const MCP_INTEGRATIONS: McpIntegration[] = [ instructions: 'Sentry advertises only a few tools directly (find_organizations, find_projects, search_issues, search_events, get_sentry_resource). Reach everything else (issue details, event stack traces, breadcrumbs, tag values, issue events, releases, traces, replays, attachments, monitors, alert rules, docs) by calling search_sentry_tools with a short query, then execute_sentry_tool with the returned tool name and arguments. Which tools exist depends on the access the admin granted when connecting. Treat Sentry as read-only unless the request explicitly asks to change Sentry state: do not resolve, assign, ignore, or otherwise update issues, and do not create or modify projects, teams, DSNs, or monitors on your own initiative.', }, + { + id: 'buildkite', + name: 'Buildkite', + url: 'https://mcp.buildkite.com/mcp/readonly', + description: `Inspect Buildkite pipelines, builds, jobs, tests, and agents from ${PRODUCT_NAME} tasks`, + icon: 'buildkite', + connectionScope: 'deployment', + oauthResource: 'https://mcp.buildkite.com/mcp/readonly', + oauthScopes: ['read'], + oauthScopeMode: 'read-only', + instructions: + "Use Buildkite to inspect organizations, pipelines, builds, jobs, logs, artifacts, annotations, tests, clusters, agents, and queues. This connection uses Buildkite's provider-enforced read-only MCP endpoint; do not assume mutation tools are available.", + }, { id: 'cloudflare', name: 'Cloudflare', diff --git a/packages/types/src/mcp-service-detection.ts b/packages/types/src/mcp-service-detection.ts index a4a8551a3a..c84c2c82cc 100644 --- a/packages/types/src/mcp-service-detection.ts +++ b/packages/types/src/mcp-service-detection.ts @@ -89,7 +89,44 @@ const X_POST_PATH_REGEX = /^\/[a-z0-9_]{1,15}\/status\/\d+/; const X_APP_PATH_REGEX = /^\/(?:search|explore)(?:\/|$)|^\/i\/(?:lists|communities|spaces)\//; +const BUILDKITE_PUBLIC_ROOT_SEGMENTS = [ + 'about', + 'blog', + 'changelog', + 'community', + 'customers', + 'docs', + 'features', + 'legal', + 'pricing', + 'resources', + 'security', + 'support', +] as const; +const BUILDKITE_ORGANIZATION_PATH_REGEX = new RegExp( + `^/(?!(?:${BUILDKITE_PUBLIC_ROOT_SEGMENTS.join('|')})(?:/|$))[^/]+(?:/|$)`, +); + export const SLACK_MCP_SETUP_SERVICES: SlackMcpSetupServiceDefinition[] = [ + { + id: 'buildkite', + name: 'Buildkite', + availabilityKind: 'curated_oauth', + hostSuffixes: ['buildkite.com'], + excludedHostnames: [ + 'www.buildkite.com', + 'api.buildkite.com', + 'mcp.buildkite.com', + ], + hostRules: [ + { + hostSuffix: 'buildkite.com', + pathRegexes: [BUILDKITE_ORGANIZATION_PATH_REGEX], + }, + ], + deploymentSettingsPath: '/integrations', + userSettingsPath: '/settings/personal', + }, { id: 'cloudflare', name: 'Cloudflare',