Skip to content
Draft
49 changes: 49 additions & 0 deletions docs/LOCAL-DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,59 @@ region you pick at login.

## Troubleshooting

### Feature flags never enabled (flag-gated UI missing)

If flag-gated surfaces (e.g. the MCP gateway behind `mcp-gateway`) never show up
even though the flag is enabled in your PostHog project, check
`VITE_POSTHOG_API_HOST` in `.env`: it must include the scheme
(`http://localhost:8010`, not `localhost:8010`). posthog-js concatenates the
host into request URLs verbatim, so a scheme-less value produces URLs like
`localhost:8010/flags/…` that the browser rejects as an invalid protocol —
every flag fetch fails silently and `isFeatureEnabled` returns `false` for
everything. Prefer `node scripts/use-local-posthog.mjs` over hand-editing; it
writes the correct form.

To confirm what the running app sees, run in the renderer console (or via CDP):

```js
posthog.config.api_host; // must start with http:// or https://
posthog.isFeatureEnabled("mcp-gateway"); // undefined ⇒ flags never loaded
```

`.env` changes need a dev-server restart (`pnpm dev`) to take effect.

### "Invalid client_id" error during OAuth

The OAuth application in your local PostHog must have the client ID `DC5uRLVbGI02YQ82grxgnK6Qn12SXWpCqdPb60oZ`. Verify at http://localhost:8010/admin/posthog/oauthapplication/.

### "OAuth error: invalid_scope"

PostHog Code requests the wildcard scope `*` (see `OAUTH_SCOPES` in
`packages/shared/src/oauth.ts`). PostHog's OAuth server only grants `*` at
`/authorize` when the OAuth application's **scope ceiling is empty** — this is
the grandfathering path for the PostHog Code client. If the application has any
explicit `scopes` or `optional_scopes` configured, the wildcard is rejected with
`invalid_scope`.

Fix: clear the scope ceiling on your local OAuth application so it matches the
production app. Either edit it at
http://localhost:8010/admin/posthog/oauthapplication/ (empty the **Scopes** and
**Optional scopes** fields), or run in your PostHog repo:

```bash
python manage.py shell -c "
from posthog.models.oauth import OAuthApplication
app = OAuthApplication.objects.get(client_id='DC5uRLVbGI02YQ82grxgnK6Qn12SXWpCqdPb60oZ')
app.scopes = []
app.optional_scopes = []
app.save()
print('cleared scope ceiling for', app.client_id)
"
```

Then retry login. (Do not add `*` to the ceiling — an explicit ceiling never
grants the wildcard, even if `*` is listed.)

### "Redirect URI mismatch"

Make sure the OAuth application's redirect URIs include `http://localhost:8237/callback` and `http://localhost:8239/callback`. Check for trailing slashes.
Expand Down
237 changes: 237 additions & 0 deletions packages/api-client/src/mcp-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Types for the team MCP gateway API (`/api/projects/{id}/mcp_gateway/*`).
// Hand-written mirrors of the Django serializers in
// products/mcp_store/backend/presentation/gateway_views.py — these endpoints
// ship behind the `mcp-gateway` flag and are not in the generated OpenAPI
// client yet.
import type { Schemas } from "./generated";
import type { McpApprovalState, McpCategory } from "./types";

export type McpGatewayUser = Schemas.UserBasic;

export type McpGatewayAuthMode = "individual" | "shared";
export type McpGatewayScopeType = "team" | "member" | "agent";
export type McpPolicyPreset = "allow" | "user" | "ask" | "block";
export type McpServiceAccountStatus = "active" | "paused";
export type McpAuditDecision = "auto" | "approved" | "pending" | "blocked";
export type McpAuditQuickFilter = "all" | "agents" | "approvals" | "blocked";
export type McpOrgRuleAudience = "everyone" | "members" | "agents";
export type McpOrgRuleEffect = "needs_approval" | "do_not_use";
export type McpPolicyDecidedBy =
| "rule"
| "scope"
| "team"
| "preset"
| "legacy"
| "default";

/** One member's personal connection to a gateway server. */
export interface McpGatewayConnection {
installation_id: string;
user: McpGatewayUser;
last_used_at: string | null;
pending_oauth: boolean;
needs_reauth: boolean;
}

/** The requesting user's own connection to a gateway server. */
export interface McpGatewayYourConnection {
installation_id: string;
scope: "personal" | "shared";
/** Per-connection switch — false when self-disabled. */
is_enabled: boolean;
pending_oauth: boolean;
needs_reauth: boolean;
last_used_at: string | null;
}

/** The admin-managed shared credential of a shared-auth server. */
export interface McpGatewaySharedCredential {
installation_id: string;
managed_by: McpGatewayUser | null;
is_enabled: boolean;
pending_oauth: boolean;
needs_reauth: boolean;
last_used_at: string | null;
}

/** One agent's access to a gateway server. */
export interface McpGatewayAgentAccess {
service_account_id: string;
name: string;
/** Agent identity handle, e.g. posthog-support. */
handle: string;
status: McpServiceAccountStatus;
last_active_at: string | null;
granted_by: McpGatewayUser | null;
}

/** A server registered in the team's gateway, with connection summary. */
export interface McpGatewayServer {
id: string;
name: string;
url: string;
description: string;
category: McpCategory;
auth_mode: McpGatewayAuthMode;
is_team_enabled: boolean;
allow_personal_connections: boolean;
icon_key: string;
docs_url: string;
template_id: string | null;
tool_count: number;
/** Members with a personal connection to this server. */
connections: McpGatewayConnection[];
your_connection: McpGatewayYourConnection | null;
/** Set when auth_mode is "shared", else null. */
shared_credential: McpGatewaySharedCredential | null;
agents: McpGatewayAgentAccess[];
/** Ids of members whose access an admin has turned off. */
revoked_user_ids: number[];
is_revoked_for_you: boolean;
created_by: McpGatewayUser | null;
created_at: string;
updated_at: string;
}

export interface McpGatewayServerUpdate {
name?: string;
description?: string;
category?: McpCategory;
/** Master switch — off means members and agents can neither see nor call the server. */
is_team_enabled?: boolean;
/** Shared-credential servers: whether members may also connect their own account. */
allow_personal_connections?: boolean;
}

/** Which policy scope a tools query or policy upsert targets. */
export interface McpGatewayPolicyScope {
scope_type?: McpGatewayScopeType;
/** Member scope target. Defaults to the requesting user. */
scope_user_id?: number;
/** Agent scope target. Required when scope_type is "agent". */
scope_service_account_id?: string;
}

export interface McpToolPolicyEntry {
tool_name: string;
policy_state: McpApprovalState;
}

/** One tool with its effective policy for the requested scope. */
export interface McpResolvedToolPolicy {
tool_name: string;
description: string;
input_schema: unknown;
policy_state: McpApprovalState;
/** What the team-level chain yields, ignoring the scope. Null when the team imposes nothing. */
team_state: McpApprovalState | null;
/** True when a rule or Blocked team ceiling leaves no editable state. */
locked: boolean;
decided_by: McpPolicyDecidedBy;
/** Matching org rule name, when decided_by is "rule". */
rule_name: string;
rule_description: string;
}

export interface McpServiceAccount {
id: string;
name: string;
description: string;
/** Stable identity handle the agent authenticates as, e.g. posthog-support. */
handle: string;
status: McpServiceAccountStatus;
/** Masked bearer token; the full token is only shown once. */
token_mask: string;
server_ids: string[];
last_active_at: string | null;
created_at: string;
updated_at: string;
}

export interface McpServiceAccountWithToken extends McpServiceAccount {
/** The full bearer token. Returned exactly once — on creation or rotation. */
token: string;
}

export interface McpOrgRule {
id: string;
name: string;
description: string;
applies_to: McpOrgRuleAudience;
effect: McpOrgRuleEffect;
/** fnmatch pattern against tool names. Blank matches destructive tools heuristically. */
tool_pattern: string;
enabled: boolean;
created_at: string;
updated_at: string;
}

export interface McpAuditActorServiceAccount {
id: string;
name: string;
handle: string;
}

export interface McpAuditEvent {
id: string;
created_at: string;
server_name: string;
tool_name: string;
decision: McpAuditDecision;
actor_user: McpGatewayUser | null;
actor_service_account: McpAuditActorServiceAccount | null;
/** Denormalized actor label (email or handle) that survives deletion. */
actor_label: string;
}

export interface McpAuditCounts {
all: number;
agents: number;
approvals: number;
blocked: number;
}

export interface McpAuditPage {
count: number;
results: McpAuditEvent[];
}

export interface TeamMcpGatewayConfig {
allow_custom_servers: boolean;
/** Whether members may share MCP connections with agents and manage agent tool policies. */
allow_member_agent_access: boolean;
/** Empty string until an admin applies a preset from Team settings. */
member_default_preset: McpPolicyPreset | "";
agent_default_preset: McpPolicyPreset | "";
/** Whether the requesting user can administer the gateway. */
is_admin: boolean;
}

export interface TeamMcpGatewayConfigUpdate {
allow_custom_servers?: boolean;
allow_member_agent_access?: boolean;
member_default_preset?: McpPolicyPreset;
agent_default_preset?: McpPolicyPreset;
}

/** One team member's gateway posture (admin overview). */
export interface McpGatewayMemberSummary {
user: McpGatewayUser;
is_org_admin: boolean;
/** Gateway servers the member has a personal connection to. */
connected_server_ids: string[];
/** Gateway servers an admin turned off for this member. */
revoked_server_ids: string[];
}

/** Sharing options accepted by install_custom / install_template. */
export interface McpGatewayInstallSharingOptions {
/** "personal" is per-user; "shared" is team-wide (the shared credential). */
scope?: "personal" | "shared";
/** Whether the server starts enabled for the whole team. */
team_enabled?: boolean;
/** Shared-credential servers: whether members may also connect personal accounts. */
allow_personal?: boolean;
/** Service accounts to share the server with at install time, when team settings allow it. */
agent_ids?: string[];
}
Loading
Loading