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
5 changes: 5 additions & 0 deletions .changeset/calm-notion-automations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@roomote/web": minor
---

Add a deployment-wide Notion connection for tasks and automations, with admin-configurable read-only or read-write tool access.
110 changes: 108 additions & 2 deletions apps/api/src/handlers/mcp/__tests__/integration-mcp.test.ts

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

21 changes: 12 additions & 9 deletions apps/api/src/handlers/mcp/integration-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getValidAccessToken } from '@roomote/sdk/server';
import {
getMcpIntegrationUpstreamUrl,
getMcpIntegrationConnectionScope,
getAllowedIntegrationMcpToolNames,
isMcpConnectionXConfig,
type McpIntegration,
} from '@roomote/types';
Expand Down Expand Up @@ -76,20 +77,24 @@ async function resolveUpstreamAccessToken(
};
}

async function resolveDeploymentDisabledToolNames(
mcpId: string,
): Promise<string[] | null> {
async function resolveDeploymentToolPolicy(mcpId: string) {
const enablement = await db.query.deploymentMcpEnablements.findFirst({
where: and(
eq(deploymentMcpEnablements.mcpId, mcpId),
eq(deploymentMcpEnablements.enabled, true),
),
columns: {
disabledTools: true,
toolAccessMode: true,
},
});

return enablement?.disabledTools ?? null;
return {
disabledToolNames: enablement?.disabledTools ?? null,
allowedToolNames:
getAllowedIntegrationMcpToolNames(mcpId, enablement?.toolAccessMode) ??
null,
};
}

export function createIntegrationMcpProxy(
Expand Down Expand Up @@ -128,17 +133,15 @@ export function createIntegrationMcpProxy(
: await resolveActingUserId(auth);

let accessToken: string | null;
let disabledToolNames: string[] | null = null;
let toolPolicy: Awaited<ReturnType<typeof resolveDeploymentToolPolicy>>;
try {
const resolvedConnection = await resolveUpstreamAccessToken(
integration.id,
upstreamUrl,
actingUserId,
);
accessToken = resolvedConnection.accessToken;
disabledToolNames = await resolveDeploymentDisabledToolNames(
integration.id,
);
toolPolicy = await resolveDeploymentToolPolicy(integration.id);
} catch (error) {
if (error instanceof McpProxyError) {
throw error;
Expand All @@ -163,7 +166,7 @@ export function createIntegrationMcpProxy(

return {
authHeader: accessToken,
disabledToolNames,
...toolPolicy,
};
},
});
Expand Down
13 changes: 11 additions & 2 deletions apps/api/src/handlers/mcp/proxy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ interface ResolvedCredentials {
/** `null` for upstreams that take no Authorization header. */
authHeader: string | null;
extraHeaders?: Record<string, string>;
/**
* Per-request allowlist override. `null` explicitly removes a static
* allowlist, while `undefined` keeps the proxy's configured default.
*/
allowedToolNames?: readonly string[] | null;
disabledToolNames?: readonly string[] | null;
/**
* Per-request upstream URL. Required when the proxy was constructed without
Expand Down Expand Up @@ -907,8 +912,12 @@ export function createMcpProxy(config: McpProxyConfig) {
}

try {
const resolvedAllowedToolNames =
credentials.allowedToolNames === undefined
? allowedToolNames
: (credentials.allowedToolNames ?? undefined);
const effectiveAllowedToolNames = getEffectiveAllowedMcpToolNames({
allowedToolNames,
allowedToolNames: resolvedAllowedToolNames,
disabledToolNames: credentials.disabledToolNames,
});
const hasToolRestrictions = Boolean(
Expand Down Expand Up @@ -941,7 +950,7 @@ export function createMcpProxy(config: McpProxyConfig) {
if (
toolName &&
!isMcpToolAllowed(toolName, {
allowedToolNames,
allowedToolNames: resolvedAllowedToolNames,
disabledToolNames: credentials.disabledToolNames,
})
) {
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/integrations/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ from [Personal Settings](/personal-settings).
| <IntegrationName href="/integrations/linear" icon="linear" name="Linear" /> | Turning issues into Roomote work | Workspace plus user identity |
| <IntegrationName href="/integrations/monday" icon="monday" name="monday.com" /> | Board, item, and workspace context | Enable first, then teammates link accounts |
| <IntegrationName href="/integrations/neon" icon="neon" name="Neon" /> | Database inspection in Neon | Enable first, then teammates link accounts |
| <IntegrationName href="/integrations/notion" icon="notion" name="Notion" /> | Shared docs and database context | Enable first, then teammates link accounts |
| <IntegrationName href="/integrations/notion" icon="notion" name="Notion" /> | Shared docs and database context | Admin connection once |
| <IntegrationName href="/integrations/posthog" icon="posthog" name="PostHog" /> | Product analytics, experiments, and error context | Admin connection once |
| <IntegrationName href="/integrations/pylon" icon="/logo/integrations/pylon.svg" name="Pylon" /> | Customer issue and account context | Admin connection once |
| <IntegrationName href="/integrations/railway" icon="railway" name="Railway" /> | Project and service context from Railway | Admin connection once |
Expand Down
33 changes: 26 additions & 7 deletions apps/docs/integrations/notion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,40 @@ icon: 'https://api.iconify.design/simple-icons:notion.svg?color=currentColor'
---

Connect Notion when product specs, runbooks, notes, or operating context already
live there and Roomote should be able to inspect that material during a task.
live there and Roomote should be able to use that material during tasks and
automations.

## When to use it

- Pull a spec or runbook into a planning or debugging task
- Inspect database-backed project context without copying it into the prompt
- Keep task context close to the docs your team already maintains
- Optionally create or update pages from approved Roomote workflows

## How setup works

Admins enable Notion from **Settings > Integrations**. Each teammate then links
their own Notion account from [Personal Settings](/personal-settings) when they
need it.
An admin connects Notion once from **Settings > Integrations**. Roomote stores
that OAuth connection for the deployment, so interactive tasks and automations
use the same Notion workspace access without each teammate linking an account.

<Note>
After upgrading from personal Notion connections, an admin must reconnect
Notion once from **Settings > Integrations**. Roomote does not promote an
existing teammate's credential into a deployment credential automatically.
</Note>

Notion starts in **Read only** mode. In **Manage tools**, an admin can choose:

- **Read only (recommended)** — allows reviewed search and read tools while
blocking content changes
- **Read and write** — also allows tools that create, update, move, duplicate,
or comment on content

The selected mode applies to every Roomote task and automation in the
deployment. Individual tools can still be disabled to narrow access further.

## What to expect

Notion gives Roomote shared document context. The resulting engineering work
still gets reviewed in the Roomote task view and normal repository workflow.
The connection has the same Notion access as the account that authorizes it.
Use an account whose workspace permissions match what Roomote should be able to
reach, and enable read-write mode only when unattended automations are expected
to change Notion content.

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

6 changes: 1 addition & 5 deletions apps/web/src/app/(authenticated)/home/OnboardingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ const ADMIN_INTEGRATION_ORDER = [
'asana',
] as const;

const PERSONAL_MCP_INTEGRATION_ORDER = [
'notion',
'monday',
'supabase',
] as const;
const PERSONAL_MCP_INTEGRATION_ORDER = ['monday', 'supabase'] as const;

const CARD_EXIT_TRANSITION = {
duration: 0.4,
Expand Down
Loading
Loading