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',