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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ flowchart LR
| **Coalescing** | Duplicate notifications for active threads fold into existing work. |
| **Policy gates** | Trust, canary scope, actions, routes, and repo roles live in JSON policy. |
| **Safe rollout** | Replay, shadow, dry-run, canary, then live. |
| **Agent knowledge MCP** | Local agents can query acquired repository knowledge through an authenticated read-only MCP server. |
| **Agent knowledge MCP** | Agents can query acquired repository knowledge through an authenticated read-only HTTP MCP server. |
| **Automatic releases** | Conventional commits drive tags, changelog, GitHub Releases, wheel/sdist. |

## Installation
Expand Down Expand Up @@ -166,7 +166,7 @@ See [`docs/shadow-canary.md`](docs/shadow-canary.md).
| Understand the system shape | [`docs/architecture.md`](docs/architecture.md) |
| Develop or test changes | [`docs/development.md`](docs/development.md) |
| Operate the bridge | [`docs/operations.md`](docs/operations.md) |
| Expose bridge knowledge to local agents | [`docs/mcp.md`](docs/mcp.md) |
| Expose bridge knowledge to agents | [`docs/mcp.md`](docs/mcp.md) |
| Configure trust, actions, routes, roles | [`docs/policy-reference.md`](docs/policy-reference.md) |
| Plan rollout safely | [`docs/shadow-canary.md`](docs/shadow-canary.md) |
| Understand releases | [`docs/releases.md`](docs/releases.md) |
Expand Down
36 changes: 34 additions & 2 deletions dashboard/src/main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ describe("MCP access page", () => {
error={null}
user={admin}
dashboardUrl="https://bridge.example.com/ops"
dashboardUrlSource="configured"
now={Date.parse("2026-06-23T11:05:00Z")}
onCreate={onCreate}
onRevoke={onRevoke}
Expand All @@ -181,9 +182,15 @@ describe("MCP access page", () => {

expect(screen.getByText("Connect an agent")).toBeInTheDocument();
expect(screen.getByText("Public dashboard URL")).toBeInTheDocument();
expect(screen.getByText("Configured public URL")).toBeInTheDocument();
expect(screen.getByText("https://bridge.example.com/ops/mcp")).toBeInTheDocument();
expect(screen.getByText("This release exposes the MCP server over local stdio. It does not publish an HTTP MCP endpoint.")).toBeInTheDocument();
expect(screen.getByText(/\"command\": \"gab\"/)).toBeInTheDocument();
expect(screen.getByText("https://bridge.example.com/ops/api/mcp")).toBeInTheDocument();
expect(screen.queryByText(/Set GITHUB_AGENT_BRIDGE_DASHBOARD_PUBLIC_URL/)).not.toBeInTheDocument();
expect(screen.getByText("Remote agents connect directly with a bearer token; no local `gab` binary is required on the agent host.")).toBeInTheDocument();
expect(screen.getByText(/\"url\": \"https:\/\/bridge.example.com\/ops\/api\/mcp\"/)).toBeInTheDocument();
expect(screen.getByText(/\"Authorization\": \"Bearer/)).toBeInTheDocument();
expect(screen.queryByText("Local fallback")).not.toBeInTheDocument();
expect(screen.queryByText(/mcp-serve/)).not.toBeInTheDocument();

await user.type(screen.getByLabelText("Token name"), "local agent");
await user.click(screen.getByRole("button", { name: "Create token" }));
Expand All @@ -206,6 +213,7 @@ describe("MCP access page", () => {
error={null}
user={{ login: "reader", avatar_url: "", html_url: "https://github.com/reader", is_admin: false }}
dashboardUrl="https://bridge.example.com"
dashboardUrlSource="configured"
now={Date.parse("2026-06-23T11:05:00Z")}
onCreate={vi.fn()}
onRevoke={vi.fn()}
Expand All @@ -216,6 +224,30 @@ describe("MCP access page", () => {
expect(screen.getByText("Admin access is required to manage MCP tokens.")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Create token" })).not.toBeInTheDocument();
});

it("requires a configured public URL before showing a remote MCP endpoint", () => {
render(
<McpPage
tokens={[]}
loading={false}
error={null}
user={admin}
dashboardUrl="http://127.0.0.1:8765"
dashboardUrlSource="request"
now={Date.parse("2026-06-23T11:05:00Z")}
onCreate={vi.fn()}
onRevoke={vi.fn()}
onRefresh={vi.fn()}
/>,
);

expect(screen.getByText("Needs public URL")).toBeInTheDocument();
expect(screen.getByText("Set GITHUB_AGENT_BRIDGE_DASHBOARD_PUBLIC_URL or forward X-Forwarded-* headers")).toBeInTheDocument();
expect(screen.getByText("Public dashboard URL required before connecting remote agents")).toBeInTheDocument();
expect(screen.queryByText("http://127.0.0.1:8765/mcp")).not.toBeInTheDocument();
expect(screen.queryByText("http://127.0.0.1:8765/api/mcp")).not.toBeInTheDocument();
expect(screen.getByText(/\"url\": \"https:\/\/bridge.example.com\/api\/mcp\"/)).toBeInTheDocument();
});
});

describe("status badges", () => {
Expand Down
52 changes: 33 additions & 19 deletions dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type DashboardStatus = {
service: string;
read_only: boolean;
dashboard_url?: string;
dashboard_url_source?: "configured" | "forwarded" | "request";
admin_actions: string[];
autoupdate: AutoupdateState;
metrics?: {
Expand Down Expand Up @@ -934,6 +935,7 @@ function App() {
error={mcpTokens.error}
user={me.data?.user}
dashboardUrl={dashboardStatus.data?.dashboard_url}
dashboardUrlSource={dashboardStatus.data?.dashboard_url_source}
now={now}
onCreate={createMcpToken}
onRevoke={revokeMcpToken}
Expand Down Expand Up @@ -1831,6 +1833,7 @@ function McpPage({
error,
user,
dashboardUrl,
dashboardUrlSource,
now,
onCreate,
onRevoke,
Expand All @@ -1841,6 +1844,7 @@ function McpPage({
error: Error | null;
user: UserProfile | undefined;
dashboardUrl?: string;
dashboardUrlSource?: "configured" | "forwarded" | "request";
now: number;
onCreate: (name: string) => Promise<McpTokenCreateResponse>;
onRevoke: (tokenId: string) => Promise<void>;
Expand All @@ -1852,21 +1856,23 @@ function McpPage({
const [createdToken, setCreatedToken] = React.useState<McpTokenCreateResponse | null>(null);
const [actionError, setActionError] = React.useState("");
const activeTokens = tokens ?? [];
const mcpDashboardUrl = `${(dashboardUrl || (typeof window === "undefined" ? "" : window.location.origin)).replace(/\/$/, "")}/mcp`;
const publicBaseUrl = (dashboardUrl || (typeof window === "undefined" ? "" : window.location.origin)).replace(/\/$/, "");
const mcpDashboardUrl = `${publicBaseUrl}/mcp`;
const mcpEndpointUrl = `${publicBaseUrl}/api/mcp`;
if (user && !user.is_admin) {
return (
<div className="grid min-w-0 gap-4">
<PageTitle icon={<KeyRound className="h-5 w-5 text-muted" aria-hidden />} title="MCP access" subtitle="Read-only local-agent access is limited to dashboard admins." action={<RefreshButton onClick={onRefresh} />} />
<PageTitle icon={<KeyRound className="h-5 w-5 text-muted" aria-hidden />} title="MCP access" subtitle="Read-only agent access is limited to dashboard admins." action={<RefreshButton onClick={onRefresh} />} />
<EmptyState text="Admin access is required to manage MCP tokens." />
</div>
);
}
return (
<div className="grid min-w-0 gap-4">
<PageTitle icon={<KeyRound className="h-5 w-5 text-muted" aria-hidden />} title="MCP access" subtitle="Issue and revoke read-only tokens for local agents." action={<RefreshButton onClick={onRefresh} />} />
<PageTitle icon={<KeyRound className="h-5 w-5 text-muted" aria-hidden />} title="MCP access" subtitle="Issue and revoke read-only tokens for agents." action={<RefreshButton onClick={onRefresh} />} />
{error ? <Banner tone="error" text={error.message} /> : null}
{actionError ? <Banner tone="error" text={actionError} /> : null}
<McpSetupGuide dashboardUrl={mcpDashboardUrl} />
<McpSetupGuide dashboardUrl={mcpDashboardUrl} endpointUrl={mcpEndpointUrl} dashboardUrlSource={dashboardUrlSource ?? "request"} />
{createdToken ? (
<section className="rounded-md border border-emerald-300 bg-emerald-50 p-3 text-emerald-950" aria-label="Created MCP token">
<div className="flex flex-wrap items-start justify-between gap-3">
Expand Down Expand Up @@ -1929,37 +1935,45 @@ function McpPage({
);
}

function McpSetupGuide({ dashboardUrl }: { dashboardUrl: string }) {
function McpSetupGuide({ dashboardUrl, endpointUrl, dashboardUrlSource }: { dashboardUrl: string; endpointUrl: string; dashboardUrlSource: "configured" | "forwarded" | "request" }) {
const sourceLabel = dashboardUrlSource === "configured" ? "Configured public URL" : dashboardUrlSource === "forwarded" ? "Forwarded public URL" : "Needs public URL";
const needsPublicUrlConfig = dashboardUrlSource === "request";
const displayDashboardUrl = needsPublicUrlConfig ? "Set GITHUB_AGENT_BRIDGE_DASHBOARD_PUBLIC_URL or forward X-Forwarded-* headers" : dashboardUrl;
const displayEndpointUrl = needsPublicUrlConfig ? "Public dashboard URL required before connecting remote agents" : endpointUrl;
const configEndpointUrl = needsPublicUrlConfig ? "https://bridge.example.com/api/mcp" : endpointUrl;
const agentConfig = `{
"mcpServers": {
"github-agent-bridge": {
"command": "gab",
"args": ["--db", "~/.local/state/github-agent-bridge/bridge.sqlite3", "mcp-serve"],
"env": {
"GITHUB_AGENT_BRIDGE_MCP_TOKEN": "gab_mcp_..."
"url": "${configEndpointUrl}",
"headers": {
"Authorization": "Bearer \${GITHUB_AGENT_BRIDGE_MCP_TOKEN}"
}
}
}
}`;
return (
<Panel title="Connect an agent">
{needsPublicUrlConfig ? (
<Banner tone="warning" text="Set GITHUB_AGENT_BRIDGE_DASHBOARD_PUBLIC_URL, or forward X-Forwarded-* headers from the proxy, before connecting remote agents." />
) : null}
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="grid min-w-0 gap-3">
<div className="min-w-0 rounded-md border border-border bg-white p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold">
<ExternalLink className="h-4 w-4 text-muted" aria-hidden />
Public dashboard URL
<span className="rounded-sm border border-border bg-slate-50 px-1.5 py-0.5 font-mono text-[11px] font-normal text-muted">{sourceLabel}</span>
</div>
<p className="mt-1 text-xs text-muted">Share this proxy-aware page URL with bridge admins who need to issue or revoke MCP tokens.</p>
<pre className="mt-3 overflow-auto rounded-md bg-slate-950 px-3 py-2 font-mono text-xs text-slate-100">{dashboardUrl}</pre>
<p className="mt-1 text-xs text-muted">Share this page URL with bridge admins only after it resolves to the external dashboard origin.</p>
<pre className="mt-3 overflow-auto rounded-md bg-slate-950 px-3 py-2 font-mono text-xs text-slate-100">{displayDashboardUrl}</pre>
</div>
<div className="min-w-0 rounded-md border border-border bg-white p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<TerminalSquare className="h-4 w-4 text-muted" aria-hidden />
Transport
<ExternalLink className="h-4 w-4 text-muted" aria-hidden />
HTTP MCP endpoint
</div>
<p className="mt-1 text-xs text-muted">This release exposes the MCP server over local stdio. It does not publish an HTTP MCP endpoint.</p>
<pre className="mt-3 overflow-auto rounded-md bg-slate-950 px-3 py-2 font-mono text-xs text-slate-100">gab --db ~/.local/state/github-agent-bridge/bridge.sqlite3 mcp-serve</pre>
<p className="mt-1 text-xs text-muted">Remote agents connect directly with a bearer token; no local `gab` binary is required on the agent host.</p>
<pre className="mt-3 overflow-auto rounded-md bg-slate-950 px-3 py-2 font-mono text-xs text-slate-100">{displayEndpointUrl}</pre>
</div>
</div>
<div className="grid min-w-0 gap-3">
Expand All @@ -1968,7 +1982,7 @@ function McpSetupGuide({ dashboardUrl }: { dashboardUrl: string }) {
<KeyRound className="h-4 w-4 text-muted" aria-hidden />
Agent config
</div>
<p className="mt-1 text-xs text-muted">Create a token below, then store the one-time secret in the agent environment.</p>
<p className="mt-1 text-xs text-muted">Create a token below, then store the one-time secret as `GITHUB_AGENT_BRIDGE_MCP_TOKEN` for the agent.</p>
<pre className="mt-3 max-h-64 overflow-auto rounded-md bg-slate-950 px-3 py-2 font-mono text-xs leading-relaxed text-slate-100">{agentConfig}</pre>
</div>
<div className="min-w-0 rounded-md border border-border bg-white p-3">
Expand Down Expand Up @@ -3318,8 +3332,8 @@ function EmptyState({ text }: { text: string }) {
return <div className="rounded-md border border-dashed border-border p-6 text-center text-sm text-muted">{text}</div>;
}

function Banner({ tone, text }: { tone: "error"; text: string }) {
return <div className={cn("rounded-md border p-3 text-sm", tone === "error" && "border-red-300 bg-red-50 text-red-700")}>{text}</div>;
function Banner({ tone, text }: { tone: "error" | "warning"; text: string }) {
return <div className={cn("rounded-md border p-3 text-sm", tone === "error" && "border-red-300 bg-red-50 text-red-700", tone === "warning" && "border-amber-300 bg-amber-50 text-amber-800")}>{text}</div>;
}

function RefreshButton({ onClick, compactOnMobile = false }: { onClick: () => void; compactOnMobile?: boolean }) {
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A compact map of the `github-agent-bridge` documentation set.
| Roll out safely | [`shadow-canary.md`](shadow-canary.md) | How-to |
| Operate production | [`operations.md`](operations.md) | How-to |
| Configure dashboard GitHub OAuth | [`dashboard-github-oauth.md`](dashboard-github-oauth.md) | How-to |
| Expose bridge knowledge to local agents | [`mcp.md`](mcp.md) | How-to |
| Expose bridge knowledge to agents | [`mcp.md`](mcp.md) | How-to |
| Develop the bridge | [`development.md`](development.md) | How-to |
| Diagnose known failures | [`failure-modes.md`](failure-modes.md) | Reference |
| Understand release automation | [`releases.md`](releases.md) | Reference |
Expand Down
39 changes: 29 additions & 10 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# MCP server

`github-agent-bridge` includes a stdio MCP server for local agents that need
read-only access to acquired bridge knowledge.
`github-agent-bridge` includes an authenticated read-only MCP server for agents
that need access to acquired bridge knowledge. The dashboard exposes the server
over HTTP so agents can connect without installing or launching `gab`.

## Create a token

Expand Down Expand Up @@ -31,23 +32,41 @@ Dashboard admins can manage the same records through:
- `POST /api/mcp/tokens`
- `DELETE /api/mcp/tokens/{token_id}`

The dashboard MCP page shows a public dashboard URL for sharing this token
management page with admins. Behind a reverse proxy, set
The dashboard MCP page shows both a public dashboard URL for token management
and the public MCP endpoint URL. Behind a reverse proxy, set
`GITHUB_AGENT_BRIDGE_DASHBOARD_PUBLIC_URL` to the external origin, for example
`https://bridge.example.com`. If that variable is unset, the dashboard derives
the origin from `X-Forwarded-Proto`, `X-Forwarded-Host`, and
`X-Forwarded-Prefix` when the proxy sends them.

## Run the server
## Connect over HTTP

Configure the local MCP client to launch:
Remote agents should connect to the dashboard endpoint:

```bash
gab --db ~/.local/state/github-agent-bridge/bridge.sqlite3 mcp-serve
```text
https://bridge.example.com/api/mcp
```

`mcp-serve` rejects startup unless `GITHUB_AGENT_BRIDGE_MCP_TOKEN` or
`--token` matches an active token in the bridge database.
Authenticate every MCP request with:

```text
Authorization: Bearer gab_mcp_...
```

For clients that accept JSON MCP server config:

```json
{
"mcpServers": {
"github-agent-bridge": {
"url": "https://bridge.example.com/api/mcp",
"headers": {
"Authorization": "Bearer ${GITHUB_AGENT_BRIDGE_MCP_TOKEN}"
}
}
}
}
```

## Exposed capabilities

Expand Down
Loading
Loading