Skip to content
This repository was archived by the owner on Jul 3, 2026. It is now read-only.
Open
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
31 changes: 16 additions & 15 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,22 @@ the frontend from the platform's webview origin instead:
A `fetch('http://127.0.0.1:19789/.well-known/oauth-protected-resource')`
from any of those origins is cross-origin (different scheme and/or host),
so the browser/webview will block it without CORS headers — exactly the
same failure mode as issue #14 in dev. `App.tsx` therefore keeps the direct
URL in production builds (gated on `import.meta.env.DEV`); the status card
will degrade to "Failed" rather than 404 through a proxy prefix that does
not exist outside the dev server.

This is a **known follow-up**, not a silent assumption of safety:

- The MCP server contract stays locked (no `Access-Control-Allow-Origin`
on `/mcp` or `/.well-known/oauth-protected-resource`) — those endpoints
expose project timeline / state and must not be reachable from arbitrary
browser origins sharing the host.
- The planned production fix is to route the status probe through a Tauri
Rust command (or `tauri-plugin-http`) so the fetch executes from the
Rust core, not the webview. That preserves the locked MCP contract and
removes the cross-origin fetch entirely. Tracked separately from #14.
same failure mode as issue #14 in dev.

**Fix (issue #20):** production builds use a Tauri Rust command
(`probe_oauth_protected_resource`) instead of a browser `fetch()`. The
command runs in the Rust process (same process as the MCP server), so
there is no cross-origin fetch — the browser's same-origin policy does not
apply. The MCP server contract stays locked (no `Access-Control-Allow-Origin`
on `/mcp` or `/.well-known/oauth-protected-resource`).

- **Command:** `app/src-tauri/src/commands/mod.rs` —
`#[tauri::command] probe_oauth_protected_resource` performs an HTTP GET
to `http://127.0.0.1:19789/.well-known/oauth-protected-resource` via
`reqwest` and returns `{ status, body, error }` to the frontend.
- **Frontend:** `App.tsx` branches on `import.meta.env.DEV` — dev uses
`fetch()` through the Vite proxy (unchanged from PR #17), production uses
`invoke('probe_oauth_protected_resource')` via `@tauri-apps/api/core`.

## Port status

Expand Down
11 changes: 11 additions & 0 deletions app/frontend/package-lock.json

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

1 change: 1 addition & 0 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
Expand Down
118 changes: 79 additions & 39 deletions app/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { invoke } from '@tauri-apps/api/core'
import './App.css'

/**
Expand All @@ -16,27 +17,35 @@
* same-origin from Vite's dev server, sidestepping CORS without weakening
* the MCP server's security posture.
*
* PROD: There is no Vite dev server in a bundled Tauri build, so the proxy
* is not available. The Tauri webview origin in production is
* `tauri://localhost` (macOS) / `https://tauri.localhost` (Windows) /
* `http://tauri.localhost` (Linux) — a cross-origin fetch to
* `http://127.0.0.1:19789` from those origins will hit the same CORS wall
* as dev. This is a known follow-up: see the "MCP fetch in production"
* section of app/README.md for the tracking note and planned fix
* (tauri-plugin-http or a Tauri Rust command that performs the fetch on
* the Rust side). For now production builds keep the direct URL so the
* status card degrades cleanly to "Failed" rather than 404ing through a
* proxy prefix that doesn't exist.
* PROD: The Vite dev-server proxy is not available in a bundled Tauri
* build. The Tauri webview origin in production is `tauri://localhost`
* (macOS) / `https://tauri.localhost` (Windows) / `http://tauri.localhost`
* (Linux) — a cross-origin fetch to `http://127.0.0.1:19789` from those
* origins would be blocked by the browser (same CORS wall as dev, since
* the MCP server contract does NOT send CORS headers). Instead of a
* browser `fetch()`, production uses a Tauri Rust command
* (`probe_oauth_protected_resource`) that performs the HTTP GET from the
* Rust process (same process as the MCP server). This bypasses the
* browser's same-origin policy entirely — there is no cross-origin fetch.
* See `app/src-tauri/src/commands/mod.rs` for the command implementation.
*/
const MCP_OAUTH_RESOURCE_URL = import.meta.env.DEV
? '/mcp-api/.well-known/oauth-protected-resource'
: 'http://127.0.0.1:19789/.well-known/oauth-protected-resource'
const MCP_OAUTH_RESOURCE_URL = '/mcp-api/.well-known/oauth-protected-resource'

/** Expected shape of the RFC 9728 `oauth-protected-resource` response. */
interface OAuthProtectedResource {
resource: string
}

/**
* Shape returned by the Tauri `probe_oauth_protected_resource` command.
* Must match `ProbeResult` in `app/src-tauri/src/commands/mod.rs`.
*/
interface TauriProbeResult {
status: number | null
body: string | null
error: string | null
}

type ConnectionState = 'connecting' | 'connected' | 'failed'

interface McpStatus {
Expand All @@ -60,7 +69,7 @@
const [retrying, setRetrying] = useState(false)
const abortRef = useRef<AbortController | null>(null)

const probe = useCallback(async () => {

Check failure on line 72 in app/frontend/src/App.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Wolfvin_palmier-pro-windows&issues=AZ7p2PSK07zXu9atTy7S&open=AZ7p2PSK07zXu9atTy7S&pullRequest=21
// Cancel any in-flight probe before starting a new one.
abortRef.current?.abort()
const controller = new AbortController()
Expand All @@ -73,30 +82,59 @@
}))

try {
const res = await fetch(MCP_OAUTH_RESOURCE_URL, {
signal: controller.signal,
// Long-running MCP clients use the Streamable HTTP transport, but
// this probe is a one-shot GET — give it a generous timeout anyway
// in case the server is still booting.
})
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`)
if (import.meta.env.DEV) {
// ── DEV: use Vite dev-server proxy (same-origin, no CORS) ──
const res = await fetch(MCP_OAUTH_RESOURCE_URL, {
signal: controller.signal,
})
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`)
}
const text = await res.text()
let parsed: OAuthProtectedResource | null = null
try {
parsed = JSON.parse(text) as OAuthProtectedResource
} catch {
// RFC 9728 says the body MUST be JSON, but we degrade gracefully
// and surface the raw body in the detail panel.
}
const resource = parsed?.resource ?? 'unknown'
setStatus({
state: 'connected',
summary: `Connected — ${resource}`,
detail: text,
lastCheckedAt: new Date().toISOString(),
})
} else {
// ── PROD: use Tauri Rust command (same-process, no CORS) ──
// The invoke() call routes through Tauri's IPC bridge to the Rust
// `probe_oauth_protected_resource` command, which performs the
// HTTP GET from the Rust process. This avoids the browser's
// same-origin policy entirely — there is no browser fetch.
const result = await invoke<TauriProbeResult>('probe_oauth_protected_resource')

if (result.error) {
throw new Error(result.error)
}
if (result.status && result.status >= 400) {
throw new Error(`HTTP ${result.status}`)
}

const text = result.body ?? ''
let parsed: OAuthProtectedResource | null = null
try {
parsed = JSON.parse(text) as OAuthProtectedResource
} catch {
// RFC 9728 says the body MUST be JSON, but we degrade gracefully.
}
const resource = parsed?.resource ?? 'unknown'
setStatus({
state: 'connected',
summary: `Connected — ${resource}`,
detail: text,
lastCheckedAt: new Date().toISOString(),
})
}
const text = await res.text()
let parsed: OAuthProtectedResource | null = null
try {
parsed = JSON.parse(text) as OAuthProtectedResource
} catch {
// RFC 9728 says the body MUST be JSON, but we degrade gracefully
// and surface the raw body in the detail panel.
}
const resource = parsed?.resource ?? 'unknown'
setStatus({
state: 'connected',
summary: `Connected — ${resource}`,
detail: text,
lastCheckedAt: new Date().toISOString(),
})
} catch (err) {
if ((err as Error).name === 'AbortError') {
// Superseded by a newer probe — don't surface as failure.
Expand Down Expand Up @@ -166,12 +204,14 @@
: 'Not yet checked'}
{' · '}
<a
href={MCP_OAUTH_RESOURCE_URL}
href={import.meta.env.DEV ? MCP_OAUTH_RESOURCE_URL : 'http://127.0.0.1:19789/.well-known/oauth-protected-resource'}
target="_blank"
rel="noreferrer"
style={{ color: '#8ab4f8', textDecoration: 'none' }}
>
{MCP_OAUTH_RESOURCE_URL}
{import.meta.env.DEV
? MCP_OAUTH_RESOURCE_URL
: 'http://127.0.0.1:19789/.well-known/oauth-protected-resource'}
</a>
</span>
<button
Expand Down
Loading