fix(mcp): use native HTTP for Devin MCP connections (#51646)

## Problem

The Devin Desktop preset wraps Supabase MCP URLs in `npx -y mcp-remote`,
although Devin Local and Devin CLI support Streamable HTTP natively.
Connecting therefore adds a Node/npm bridge that the client does not
need. The preset also lacks Devin-specific setup and authentication
instructions.

## Solution

Use the existing native `url` configuration for Devin:

```json
{
  "mcpServers": {
    "supabase": {
      "url": "https://mcp.supabase.com/mcp"
    }
  }
}
```

The generated URL retains the selected project, read-only mode, and
features. The internal `windsurf` key is preserved.

Add shared setup instructions for the configuration panel and generated
Markdown: user-scope installation, the Windows config file location next
to the shared macOS/Linux one, a re-authentication command for hosted
connections, and loading tools in a new Devin Local or CLI session.
Local connections omit the re-authentication step.

Include Devin in the shared client URL round-trip test. The Devin-only
test files from the first revision were removed in review so that tests
stay shared across clients.

Native configuration follows the official [Devin MCP
documentation](https://docs.devin.ai/cli/extensibility/mcp/configuration)
and [Devin Local
integration](https://docs.devin.ai/desktop/devin-local).

## Validation

- **273 tests passed** across 28 `ui-patterns` test files on the current
revision; 34 of them in the MCP section.
- Package typechecking, Prettier on the four changed files, and `git
diff --check` passed.
- **Devin CLI 3000.11.3:** native HTTP initialization and tool
discovery; synthetic OAuth with S256 PKCE and authenticated tool
discovery using a saved token.
- **Devin Desktop 3.10.48 / bundled Devin Local 3000.10.48:** the server
card showed `Connected` and the fixture tool. Its `tools/list` request
came from the bundled Devin Local process.

OAuth validation used controlled local fixtures; live Supabase Hosted
authorization was not exercised. The full monorepo application build was
not run.

## Review instructions

1. Run `pnpm --filter ui-patterns exec vitest run src/McpUrlBuilder` and
`pnpm --filter ui-patterns typecheck`.
2. Select **Devin Desktop** in the MCP configuration panel. Confirm that
the JSON uses `url` and preserves the selected project, read-only mode,
and features.
3. Switch to the local connection. Confirm that Hosted login
instructions disappear. Check that generated Markdown explicitly limits
that login step to Hosted connections.

## Checklist

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide).
N/A: no docs topic edited; the change is in the shared MCP configuration
component and its generated instructions.
This commit is contained in:
Alex Gercog authored and GitHub committed 2026-10-14 15:19:04 +03:00
1 parent 09a1f431bc
commit 0f618d8efb
4 files changed
+46 -38

No files matched your search

@@ -16,7 +16,6 @@ import type {
OmpMcpConfig,
OpenCodeMcpConfig,
VSCodeMcpConfig,
WindsurfMcpConfig,
} from './types'
import { getMcpUrl } from './types'
@@ -248,17 +247,7 @@ export const MCP_CLIENT_DATA: McpClientData[] = [
icon: 'devin',
hasDistinctDarkIcon: true,
configFile: '~/.config/devin/mcp_config.json',
externalDocsUrl: '',
transformConfig: (config): WindsurfMcpConfig => {
return {
mcpServers: {
supabase: {
command: 'npx',
args: ['-y', 'mcp-remote', config.mcpServers.supabase.url],
},
},
}
},
externalDocsUrl: 'https://docs.devin.ai/cli/extensibility/mcp/configuration',
},
{
key: 'warp',
@@ -441,6 +430,10 @@ export const MCP_CLI_COMMANDS: Record<string, McpCliCommands> = {
install: (url) => `copilot mcp add --transport http supabase "${url}"`,
authenticate: 'copilot -i /mcp',
},
windsurf: {
install: (url) => `devin mcp add -s user supabase "${url}"`,
authenticate: 'devin mcp login supabase',
},
goose: {
install: (url) => `goose session --with-streamable-http-extension "${url}"`,
},
@@ -210,6 +210,39 @@ export const MCP_CLIENT_INSTRUCTIONS: Record<string, McpClientInstructions> = {
</>
),
},
windsurf: {
primary: ({ url }) => (
<>
<paragraph>
These instructions use the Devin Local agent in Devin Desktop or Devin CLI. Add the
Supabase MCP server to your user configuration:
</paragraph>
<code lang="bash" value={MCP_CLI_COMMANDS['windsurf'].install!(url)} />
</>
),
alternate: ({ isPlatform }) => (
<>
<paragraph>
On Windows, the configuration file is{' '}
<inlineCode value="%APPDATA%\devin\mcp_config.json" /> instead.
</paragraph>
{isPlatform && (
<>
<paragraph>
If you skipped the browser sign-in or need to re-authenticate, run:
</paragraph>
<code lang="bash" value={MCP_CLI_COMMANDS['windsurf'].authenticate!} />
<paragraph>Complete the authorization flow in your browser.</paragraph>
</>
)}
<paragraph>Check the server configuration:</paragraph>
<code lang="bash" value="devin mcp list" />
<paragraph>
Start a new Devin Local or Devin CLI session to load the server's tools.
</paragraph>
</>
),
},
goose: {
primary: ({ url }) => (
<>
@@ -5,22 +5,14 @@ import { getMcpUrl, type McpClientBaseConfig } from './types'
const SERVER_URL = 'https://mcp.supabase.com/mcp?project_ref=abc123&read_only=true'
/**
* `windsurf` configures a stdio `mcp-remote` command that carries the URL in `args`, so its
* config has no URL field to read back. It renders no instructions and no deep link, so it
* never reaches `getMcpUrl`.
*/
const CLIENTS_WITHOUT_URL_IN_CONFIG = new Set(['windsurf'])
describe('getMcpUrl', () => {
it.each(
MCP_CLIENT_DATA.filter((client) => !CLIENTS_WITHOUT_URL_IN_CONFIG.has(client.key)).map(
(client) => [client.key, client] as const
)
)('reads the server URL back out of the %s config', (_key, client) => {
const base: McpClientBaseConfig = { mcpServers: { supabase: { url: SERVER_URL } } }
const config = client.transformConfig ? client.transformConfig(base) : base
it.each(MCP_CLIENT_DATA.map((client) => [client.key, client] as const))(
'reads the server URL back out of the %s config',
(_key, client) => {
const base: McpClientBaseConfig = { mcpServers: { supabase: { url: SERVER_URL } } }
const config = client.transformConfig ? client.transformConfig(base) : base
expect(getMcpUrl(config)).toBe(SERVER_URL)
})
expect(getMcpUrl(config)).toBe(SERVER_URL)
}
)
})
@@ -64,15 +64,6 @@ export interface VSCodeMcpConfig {
}
}
export interface WindsurfMcpConfig {
mcpServers: {
supabase: {
command: 'npx'
args: ['-y', 'mcp-remote', string]
}
}
}
export interface ClaudeCodeMcpConfig extends McpClientBaseConfig {
mcpServers: {
supabase: {
@@ -247,7 +238,6 @@ export type McpClientConfig =
| OpenCodeMcpConfig
| OtherMcpConfig
| VSCodeMcpConfig
| WindsurfMcpConfig
// Type guards
export function isVSCodeMcpConfig(config: McpClientConfig): config is VSCodeMcpConfig {