Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ DEFAULT_COMPUTE_PROVIDER=docker
# GITLAB_WEBHOOK_SIGNING_TOKEN=

# Optional integrations.
# Set to true to prevent curated integrations from being configured or used.
# Existing connections remain stored and become available again once unset.
# R_CURATED_INTEGRATIONS_DISABLED=true
# SLACK_APP_ID=
# R_SLACK_SIGNING_SECRET=
# R_TELEGRAM_BOT_TOKEN=
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion apps/api/src/handlers/linear/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
PRODUCT_NAME,
restoreSnapshotResumeVisiblePromptFields,
} from '@roomote/types';
import { Env } from '@roomote/env';
import { Env, areCuratedIntegrationsDisabled } from '@roomote/env';
import {
type RoutingDebugInfo,
enqueueTask,
Expand Down Expand Up @@ -254,6 +254,10 @@ export const linear = new Hono();
* The handler must emit a "thought" activity within 10 seconds to acknowledge receipt.
*/
linear.post('/', async (c) => {
if (areCuratedIntegrationsDisabled(Env.R_CURATED_INTEGRATIONS_DISABLED)) {
return c.body(null, 204);
}

const headers = c.req.header();
const rawBody = await c.req.text();

Expand Down
18 changes: 17 additions & 1 deletion apps/api/src/handlers/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hono } from 'hono';
import { Hono, type MiddlewareHandler } from 'hono';
import { Env, areCuratedIntegrationsDisabled } from '@roomote/env';
import { isNativeMcpIntegration, MCP_INTEGRATIONS } from '@roomote/types';

import type { Variables } from '../../types';
Expand All @@ -19,6 +20,21 @@ import { vercelMcp } from './vercel';

export const mcp = new Hono<{ Variables: Variables }>();

const requireCuratedIntegrations: MiddlewareHandler<{
Variables: Variables;
}> = async (c, next) => {
if (areCuratedIntegrationsDisabled(Env.R_CURATED_INTEGRATIONS_DISABLED)) {
return c.notFound();
}

await next();
};

for (const integration of MCP_INTEGRATIONS) {
mcp.use(`/${integration.id}`, requireCuratedIntegrations);
mcp.use(`/${integration.id}/*`, requireCuratedIntegrations);
}

mcp.route('/asana', asanaMcp);
mcp.route('/grafana', grafanaMcp);
mcp.route('/linear', linearMcp);
Expand Down
15 changes: 14 additions & 1 deletion apps/api/src/handlers/mcp/routing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hono } from 'hono';
import { Hono, type MiddlewareHandler } from 'hono';
import { Env, areCuratedIntegrationsDisabled } from '@roomote/env';

import type { Variables } from '../../types';

Expand All @@ -15,7 +16,19 @@ import { roomoteMcp } from './roomote';
*/
export const mcpRouting = new Hono<{ Variables: Variables }>();

const requireCuratedIntegrations: MiddlewareHandler<{
Variables: Variables;
}> = async (c, next) => {
if (areCuratedIntegrationsDisabled(Env.R_CURATED_INTEGRATIONS_DISABLED)) {
return c.notFound();
}

await next();
};

mcpRouting.route('/roomote', roomoteMcp);
mcpRouting.use('/linear', requireCuratedIntegrations);
mcpRouting.use('/linear/*', requireCuratedIntegrations);
mcpRouting.route(
'/linear',
createLinearMcp({
Expand Down
1 change: 1 addition & 0 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ as per-task auth tokens or workspace paths.
| `R_INSTANCE_ID` | Optional | Stable anonymous deployment identifier sent with telemetry and version checks. Use a random, non-identifying value when overriding it. |
| `ROOMOTE_FORCE_TELEMETRY` | Development only | Force-enables telemetry in development or preview environments when a Ping endpoint is explicitly configured. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud behavior, including required anonymous analytics and Cloud support integrations. Do not set this for self-hosted deployments. |
| `R_CURATED_INTEGRATIONS_DISABLED` | Optional | Operator policy for the curated **Settings > Integrations** catalog, which is enabled by default. Set to `true` and restart Roomote to prevent those integrations from being configured or used. Existing connections remain stored while disabled and become available again once the value is unset. Communications, source-control, inference, sandbox providers, and environment-defined MCP servers are unaffected. |
| `R_GITHUB_APP_SLUG` | GitHub setup | GitHub App slug used by server-rendered setup and GitHub integration flows. |
| `SETUP_TOKEN` | Required (non-local) | One-time bootstrap token that admits the first admin at `/setup`. Required on every non-local deployment — tokenless bootstrap is allowed only when `NODE_ENV` is not `production` and `R_APP_ENV` is `development`, so anything running with `NODE_ENV=production` needs it. Without it, first-admin bootstrap stays closed so nobody can claim the founding-admin slot by reaching the URL first. Optional only in local development. |
| `DASHBOARD_PASSWORD` | Production | Local fallback/admin password value used by the deployment. Generate a strong secret. |
Expand Down
7 changes: 7 additions & 0 deletions apps/docs/integrations/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ This section does not list provider categories that have their own setup paths:
communications providers, source-control providers, inference providers, and
sandbox providers. Configure those from the provider-specific docs instead.

Deployment operators can prevent every integration in this curated catalog
from being configured or used by setting
`R_CURATED_INTEGRATIONS_DISABLED=true` and restarting Roomote. Existing
connections remain stored but inactive, so unsetting the value restores
them. This policy does not affect the separate provider categories above or
MCP servers defined on an environment.

## Connection patterns

You will usually see one of these setup models:
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/app/api/mcp-oauth/callback/__tests__/route.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions apps/web/src/app/api/mcp-oauth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ export async function GET(request: NextRequest) {
const redirectToResult = (result: McpOAuthResult) =>
redirectWithMcpResult(webUrl, redirectPath, result, state);

if (webEnv.R_CURATED_INTEGRATIONS_DISABLED === true) {
return redirectToResult({ status: 'error', reason: 'callback_failed' });
}

// Handle OAuth errors
if (error) {
const reason: McpOAuthErrorReason =
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ export async function GET(
DEFAULT_REDIRECT_PATH;
const replayToken = requestUrl.searchParams.get('replayToken');

if (webEnv.R_CURATED_INTEGRATIONS_DISABLED === true) {
return NextResponse.redirect(
withMcpQuery(webUrl, redirectPath, 'error', 'disabled'),
);
}

const authResult = await authorize();
if (!authResult.success) {
return NextResponse.redirect(
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/app/api/mcp-oauth/replay/[token]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export async function GET(
const webEnv = await bootstrapWebRuntimeEnv();
const webUrl = getPublicAppUrl(webEnv);
const { token } = await params;

if (webEnv.R_CURATED_INTEGRATIONS_DISABLED === true) {
return NextResponse.redirect(
new URL('/error?message=Integrations are disabled', webUrl),
);
}

const replay = await getMcpOauthReplay(token);

if (!replay) {
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/settings/Integrations.test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions apps/web/src/components/settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import {
useAsanaConnection,
useConnectMcp,
useCuratedIntegrationsAvailability,
useDisconnectMcp,
useGrafanaConnection,
useDeploymentMcpEnablements,
Expand All @@ -43,6 +44,9 @@ import {
} from '@/types';

import {
Alert,
AlertDescription,
AlertTitle,
BasicTooltip,
Button,
Card,
Expand Down Expand Up @@ -1148,6 +1152,7 @@ export function Integrations() {
const disconnectLinear = useDisconnectLinear();

const deploymentEnablements = useDeploymentMcpEnablements();
const integrationsAvailability = useCuratedIntegrationsAvailability();
const oauthReadiness = useMcpOauthReadiness();
const linearOauthStatus = oauthReadiness.data?.find(
(entry) => entry.mcpId === 'linear',
Expand Down Expand Up @@ -1988,6 +1993,18 @@ export function Integrations() {
});
};

if (integrationsAvailability.data?.enabled === false) {
return (
<Alert>
<AlertTitle>Integrations disabled by deployment operator</AlertTitle>
<AlertDescription>
Curated integrations cannot be connected or used on this Roomote
instance.
</AlertDescription>
</Alert>
);
}

return (
<div className="space-y-8">
<McpToolManagementDialog
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/hooks/mcp-connections/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Queries
export { useDeploymentMcpEnablements } from './useDeploymentMcpEnablements';
export { useCuratedIntegrationsAvailability } from './useCuratedIntegrationsAvailability';
export { useUserMcpConnections } from './useUserMcpConnections';
export { useMcpConnectionTools } from './useMcpConnectionTools';
export { useMcpOauthReadiness } from './useMcpOauthReadiness';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

import { useQuery } from '@tanstack/react-query';

import { useTRPC } from '@/trpc/client';

export function useCuratedIntegrationsAvailability() {
const trpc = useTRPC();

return useQuery(trpc.mcpConnections.availability.queryOptions());
}
20 changes: 20 additions & 0 deletions apps/web/src/lib/server/curated-integrations.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions apps/web/src/lib/server/curated-integrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Env, areCuratedIntegrationsDisabled } from './env';

export const CURATED_INTEGRATIONS_DISABLED_MESSAGE =
'Integrations are disabled by the deployment operator.';

export function assertCuratedIntegrationsEnabled(
disabledFlag:
| string
| boolean
| undefined = Env.R_CURATED_INTEGRATIONS_DISABLED,
) {
if (areCuratedIntegrationsDisabled(disabledFlag)) {
throw new Error(CURATED_INTEGRATIONS_DISABLED_MESSAGE);
}
}
Loading
Loading