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
6 changes: 6 additions & 0 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -7944,6 +7944,12 @@ func applyMCPSessionFilters(query *gorm.DB, params MCPSessionsFilterParams, t mc
if len(params.MCPClientIDs) > 0 {
query = query.Where(t.table+".mcp_client_id IN ?", params.MCPClientIDs)
}
if len(params.VirtualKeyIDs) > 0 {
query = query.Where(t.table+".virtual_key_id IN ?", params.VirtualKeyIDs)
}
if len(params.UserIDs) > 0 {
query = query.Where(t.table+".user_id IN ?", params.UserIDs)
}
if params.Identity != "" {
// Exact match against whichever identity column carries the value for this
// row's mode. Parenthesized explicitly so the OR group ANDs cleanly with the
Expand Down
14 changes: 9 additions & 5 deletions framework/configstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,19 @@ type CustomersQueryParams struct {
// the virtual key's id/name (joined). Empty filter slices match all
// values for that field.
type MCPSessionsFilterParams struct {
Search string
Statuses []string
AuthModes []string // matched against auth_mode (tokens, credentials) or flow_mode (sessions, flows)
MCPClientIDs []string
Search string
Statuses []string
AuthModes []string // matched against auth_mode (tokens, credentials) or flow_mode (sessions, flows)
MCPClientIDs []string
VirtualKeyIDs []string // exact-match against virtual_key_id; only meaningful for vk-mode rows
UserIDs []string // exact-match against user_id; only meaningful for user-mode rows
// Identity exact-matches a single resolved identity value against any of
// the row's identity columns (user_id, virtual_key_id, session_id). Unlike
// Search it is not a substring match — it pins the list to exactly one
// user, virtual key, or session. Typically paired with AuthModes to scope
// to that identity's rows for a known mode.
// to that identity's rows for a known mode. Distinct from UserIDs above:
// this is a single value used for deep-linking to one identity (e.g. the
// OAuth grants table's "View sessions" action), not a multi-select facet.
Identity string
// MatchedUserIDs is an optional set of user_ids that should be treated
// as a positive search hit alongside Search. Callers that maintain a
Expand Down
2 changes: 2 additions & 0 deletions transports/bifrost-http/handlers/mcpsessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ func parseMCPSessionsListQuery(ctx *fasthttp.RequestCtx) (mcpSessionsListQuery,
q.Filters.Statuses = parseCommaSeparated(string(args.Peek("status")))
q.Filters.AuthModes = parseCommaSeparated(string(args.Peek("auth_mode")))
q.Filters.MCPClientIDs = parseCommaSeparated(string(args.Peek("mcp_client_id")))
q.Filters.VirtualKeyIDs = parseCommaSeparated(string(args.Peek("virtual_key_id")))
q.Filters.UserIDs = parseCommaSeparated(string(args.Peek("user_id")))
q.Kinds = parseCommaSeparated(string(args.Peek("kind")))
if s := string(args.Peek("limit")); s != "" {
n, err := strconv.Atoi(s)
Expand Down
156 changes: 77 additions & 79 deletions ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,31 +239,31 @@ export default function MCPClientSheet({
allowed_extra_headers: mcpClient.config.allowed_extra_headers || [],
oauth_config: supportsOAuthCredentialUpdate
? {
client_id: mcpClient.config.oauth_client_id,
client_secret: mcpClient.config.oauth_client_secret,
authorize_url: mcpClient.config.oauth_authorize_url,
token_url: mcpClient.config.oauth_token_url,
registration_url: mcpClient.config.oauth_registration_url,
resource: mcpClient.config.oauth_resource,
}
client_id: mcpClient.config.oauth_client_id,
client_secret: mcpClient.config.oauth_client_secret,
authorize_url: mcpClient.config.oauth_authorize_url,
token_url: mcpClient.config.oauth_token_url,
registration_url: mcpClient.config.oauth_registration_url,
resource: mcpClient.config.oauth_resource,
}
: undefined,
// Unlike oauth_config, token_exchange is replaced wholesale server-side
// (only client_id/client_secret get redacted-value preservation) — so
// every field, not just the ones the user edits, must be pre-populated
// with its current stored value rather than left blank.
token_exchange: supportsTokenExchangeCredentialUpdate
? {
audience: mcpClient.config.token_exchange?.audience,
client_id: mcpClient.config.token_exchange?.client_id,
client_secret: mcpClient.config.token_exchange?.client_secret,
authorization_server_url: mcpClient.config.token_exchange?.authorization_server_url,
}
audience: mcpClient.config.token_exchange?.audience,
client_id: mcpClient.config.token_exchange?.client_id,
client_secret: mcpClient.config.token_exchange?.client_secret,
authorization_server_url: mcpClient.config.token_exchange?.authorization_server_url,
}
: undefined,
tls_config: mcpClient.config.tls_config
? {
insecure_skip_verify: mcpClient.config.tls_config.insecure_skip_verify,
ca_cert_pem: mcpClient.config.tls_config.ca_cert_pem,
}
insecure_skip_verify: mcpClient.config.tls_config.insecure_skip_verify,
ca_cert_pem: mcpClient.config.tls_config.ca_cert_pem,
}
: undefined,
},
});
Expand All @@ -289,31 +289,31 @@ export default function MCPClientSheet({
allowed_extra_headers: mcpClient.config.allowed_extra_headers || [],
oauth_config: supportsOAuthCredentialUpdate
? {
client_id: mcpClient.config.oauth_client_id,
client_secret: mcpClient.config.oauth_client_secret,
authorize_url: mcpClient.config.oauth_authorize_url,
token_url: mcpClient.config.oauth_token_url,
registration_url: mcpClient.config.oauth_registration_url,
resource: mcpClient.config.oauth_resource,
}
client_id: mcpClient.config.oauth_client_id,
client_secret: mcpClient.config.oauth_client_secret,
authorize_url: mcpClient.config.oauth_authorize_url,
token_url: mcpClient.config.oauth_token_url,
registration_url: mcpClient.config.oauth_registration_url,
resource: mcpClient.config.oauth_resource,
}
: undefined,
// Unlike oauth_config, token_exchange is replaced wholesale server-side
// (only client_id/client_secret get redacted-value preservation) — so
// every field, not just the ones the user edits, must be pre-populated
// with its current stored value rather than left blank.
token_exchange: supportsTokenExchangeCredentialUpdate
? {
audience: mcpClient.config.token_exchange?.audience,
client_id: mcpClient.config.token_exchange?.client_id,
client_secret: mcpClient.config.token_exchange?.client_secret,
authorization_server_url: mcpClient.config.token_exchange?.authorization_server_url,
}
audience: mcpClient.config.token_exchange?.audience,
client_id: mcpClient.config.token_exchange?.client_id,
client_secret: mcpClient.config.token_exchange?.client_secret,
authorization_server_url: mcpClient.config.token_exchange?.authorization_server_url,
}
: undefined,
tls_config: mcpClient.config.tls_config
? {
insecure_skip_verify: mcpClient.config.tls_config.insecure_skip_verify,
ca_cert_pem: mcpClient.config.tls_config.ca_cert_pem,
}
insecure_skip_verify: mcpClient.config.tls_config.insecure_skip_verify,
ca_cert_pem: mcpClient.config.tls_config.ca_cert_pem,
}
: undefined,
});
}, [form, mcpClient, supportsOAuthCredentialUpdate, supportsTokenExchangeCredentialUpdate]);
Expand Down Expand Up @@ -391,9 +391,9 @@ export default function MCPClientSheet({
? undefined
: oauthScopesRaw.trim()
? oauthScopesRaw
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
// Only rotate when the user actually changed a field, and never
// alongside a disable (the backend rejects that combination
Expand All @@ -410,9 +410,9 @@ export default function MCPClientSheet({
const shouldUpdateTokenExchange = supportsTokenExchangeCredentialUpdate && tokenExchangeCredentialsDirty;
const tokenExchangeScopes = tokenExchangeScopesRaw.trim()
? tokenExchangeScopesRaw
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
await updateMCPClient({
id: mcpClient.config.client_id,
Expand All @@ -436,30 +436,30 @@ export default function MCPClientSheet({
allowed_extra_headers: data.allowed_extra_headers,
oauth_config: shouldRotateOAuthCredentials
? {
client_id: oauthClientID,
client_secret: oauthClientSecret,
authorize_url: data.oauth_config?.authorize_url || undefined,
token_url: data.oauth_config?.token_url || undefined,
registration_url: data.oauth_config?.registration_url || undefined,
scopes: oauthScopes,
resource: data.oauth_config?.resource || undefined,
}
client_id: oauthClientID,
client_secret: oauthClientSecret,
authorize_url: data.oauth_config?.authorize_url || undefined,
token_url: data.oauth_config?.token_url || undefined,
registration_url: data.oauth_config?.registration_url || undefined,
scopes: oauthScopes,
resource: data.oauth_config?.resource || undefined,
}
: undefined,
token_exchange: shouldUpdateTokenExchange
? {
audience: data.token_exchange?.audience?.trim() || "",
client_id: data.token_exchange?.client_id ?? { value: "", ref: "" },
client_secret: data.token_exchange?.client_secret,
authorization_server_url: data.token_exchange?.authorization_server_url?.trim() || undefined,
scopes: tokenExchangeScopes,
}
audience: data.token_exchange?.audience?.trim() || "",
client_id: data.token_exchange?.client_id ?? { value: "", ref: "" },
client_secret: data.token_exchange?.client_secret,
authorization_server_url: data.token_exchange?.authorization_server_url?.trim() || undefined,
scopes: tokenExchangeScopes,
}
: undefined,
tls_config:
data.tls_config !== undefined
? {
insecure_skip_verify: data.tls_config.insecure_skip_verify ?? false,
ca_cert_pem: data.tls_config.ca_cert_pem,
}
insecure_skip_verify: data.tls_config.insecure_skip_verify ?? false,
ca_cert_pem: data.tls_config.ca_cert_pem,
}
: undefined,
vk_configs: vkConfigsDirty ? vkConfigs : undefined,
},
Expand Down Expand Up @@ -690,7 +690,7 @@ export default function MCPClientSheet({
<span className="font-mono break-all">
{mcpClient.config.connection_type === "stdio"
? `${mcpClient.config.stdio_config?.command ?? ""} ${(mcpClient.config.stdio_config?.args ?? []).join(" ")}`.trim() ||
"-"
"-"
: mcpClient.config.connection_string?.type === "env" || mcpClient.config.connection_string?.type === "vault"
? mcpClient.config.connection_string.ref
: mcpClient.config.connection_string?.value || "-"}
Expand All @@ -709,7 +709,7 @@ export default function MCPClientSheet({
return [name, valueParts.join("=")];
}),
)}
onChange={() => {}}
onChange={() => { }}
fixedKeys={mcpClient.config.stdio_config.envs.map((env) => env.split("=")[0])}
valuePlaceholder="—"
label=""
Expand Down Expand Up @@ -991,27 +991,25 @@ export default function MCPClientSheet({
description="Static headers and header-based access rules sent with every request to this server."
testId="headers-heading"
/>
<div className="rounded-md border p-4">
<FormField
control={form.control}
name="headers"
render={({ field }) => (
<FormItem className="flex flex-col gap-3">
<FormControl>
<HeadersTable
value={field.value || {}}
onChange={field.onChange}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
label=""
useSecretVarInput
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="headers"
render={({ field }) => (
<FormItem className="flex flex-col gap-3">
<FormControl>
<HeadersTable
value={field.value || {}}
onChange={field.onChange}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
label=""
useSecretVarInput
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>

{mcpClient.config.auth_type === "per_user_headers" && (
Expand Down Expand Up @@ -1083,9 +1081,9 @@ export default function MCPClientSheet({
onBlur={() => {
const parsed = allowedExtraHeadersRaw.trim()
? allowedExtraHeadersRaw
.split(",")
.map((h) => h.trim())
.filter(Boolean)
.split(",")
.map((h) => h.trim())
.filter(Boolean)
: [];
field.onChange(parsed);
field.onBlur();
Expand Down
17 changes: 15 additions & 2 deletions ui/app/workspace/mcp-sessions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default function MCPSessionsPage() {
status: parseAsArrayOf(parseAsString).withDefault([]),
auth_mode: parseAsArrayOf(parseAsString).withDefault([]),
mcp_client_id: parseAsArrayOf(parseAsString).withDefault([]),
virtual_key_id: parseAsArrayOf(parseAsString).withDefault([]),
user_id: parseAsArrayOf(parseAsString).withDefault([]),
identity: parseAsString.withDefault(""),
offset: parseAsInteger.withDefault(0),
},
Expand All @@ -34,8 +36,10 @@ export default function MCPSessionsPage() {
status: urlState.status,
auth_mode: urlState.auth_mode,
mcp_client_id: urlState.mcp_client_id,
virtual_key_id: urlState.virtual_key_id,
user_id: urlState.user_id,
}),
[urlState.kind, urlState.status, urlState.auth_mode, urlState.mcp_client_id],
[urlState.kind, urlState.status, urlState.auth_mode, urlState.mcp_client_id, urlState.virtual_key_id, urlState.user_id],
);

const setFilters = useCallback(
Expand All @@ -45,6 +49,8 @@ export default function MCPSessionsPage() {
status: newFilters.status,
auth_mode: newFilters.auth_mode,
mcp_client_id: newFilters.mcp_client_id,
virtual_key_id: newFilters.virtual_key_id,
user_id: newFilters.user_id,
offset: 0,
});
},
Expand All @@ -57,6 +63,8 @@ export default function MCPSessionsPage() {
status: filters.status.length ? (filters.status as MCPSessionStatus[]) : undefined,
auth_mode: filters.auth_mode.length ? (filters.auth_mode as AuthMode[]) : undefined,
mcp_client_id: filters.mcp_client_id.length ? filters.mcp_client_id : undefined,
virtual_key_id: filters.virtual_key_id.length ? filters.virtual_key_id : undefined,
user_id: filters.user_id.length ? filters.user_id : undefined,
identity: normalizedIdentity || undefined,
limit: PAGE_SIZE,
offset: urlState.offset,
Expand Down Expand Up @@ -86,7 +94,12 @@ export default function MCPSessionsPage() {
}

const filtersActive =
filters.kind.length > 0 || filters.status.length > 0 || filters.auth_mode.length > 0 || filters.mcp_client_id.length > 0;
filters.kind.length > 0 ||
filters.status.length > 0 ||
filters.auth_mode.length > 0 ||
filters.mcp_client_id.length > 0 ||
filters.virtual_key_id.length > 0 ||
filters.user_id.length > 0;
const hasActiveFilters = !!urlState.q || filtersActive || !!normalizedIdentity;

const handleSearchChange = (value: string) => setUrlState({ q: value || null, offset: 0 });
Expand Down
Loading
Loading