feat: Add catalog tabs, flatten MCP overview, simplify collection install#1925
feat: Add catalog tabs, flatten MCP overview, simplify collection install#1925qstearns wants to merge 21 commits intofeat/collections-templatesfrom
Conversation
- Wire create/list/detail/serve hooks to real mcpRegistries SDK calls - Remove old catalogs pages (replaced by collections) - Collection cards on MCP page expand inline to show contained servers - Server cards within collections are clickable to navigate to detail - Discover tab now filters to public collections, search is functional - Add "Go to MCP" button in install dialog success state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The toolset picker now queries every project the user has access to and shows which project each server belongs to. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove description line to equalize card heights, add bg-muted/30 container with bg-card on each card for better visual grouping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Installing a collection now calls mcpRegistries.serve() to get the server list, then deployments.evolveDeployment() with upsertExternalMcps to attach each server as an external MCP proxy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server cards within expanded collections are from the registry serve API and their registrySpecifier doesn't map to a project-scoped toolset slug, so clicking them caused "toolset not found" errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary We have a bug that occurs when there are 2 separate MCPs sharing the same source. When we try to filter per server on insights it doesn't work because we don't have the MCP URL or toolset slug in our telemetry logs. This PR starts tracking toolset slug and mcp URL in our telemetry logs. Changes: - Adds `RecordToolsetSlug()` and `RecordMCPURL()` methods to `HTTPLogAttributes`, following the existing recorder pattern used for status codes, request bodies, etc. - Records the MCP server name (toolset slug) and MCP URL as telemetry attributes on tool call and resource read logs across MCP and instance call sites. ## Approach Rather than threading new parameters through gateway method signatures (`Do`, `ReadResource`, etc.) or adding fields to `ToolInfo`, the toolset slug and MCP URL are recorded directly into the attributes map at the call sites that have this context. This keeps the gateway API stable and follows the established pattern for telemetry metadata. <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/speakeasy-api/gram/pull/1903" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
## Summary - Adds a `toolset_slug` materialized column to the `telemetry_logs` ClickHouse table, extracted from `attributes.gram.toolset.slug`. - Adds a bloom filter index for efficient filtering by toolset slug. - Regenerates `materialized_columns_gen.go` to include the new column mapping. ## Context Companion to #1903 which records the toolset slug in telemetry attributes. This PR makes it a queryable/filterable field in the dashboard. <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/speakeasy-api/gram/pull/1906" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
…1898) ## Summary - When `defaultEnvironmentId` is not set in the `mcpMetadata.get` response, the UI incorrectly showed the "default" environment as attached (with a green/yellow dot) instead of showing an "Attach" button - Root cause: `mcpAttachedEnvironmentSlug || defaultEnvironmentSlug || "default"` fallback chain made `null` (no environment attached) indistinguishable from "default environment is attached" - Fixed in three files: `EnvironmentSwitcher.tsx`, `EnvironmentVariableRow.tsx`, `MCPEnvironmentSettings.tsx` ## Changes - **EnvironmentSwitcher**: Separated sort-order fallback from attachment state — dot indicator and Attach button now correctly reflect actual attachment status - **EnvironmentVariableRow**: Removed fallback so editing is enabled on all environments when none is attached (previously only "default" was editable) - **MCPEnvironmentSettings**: `attachedEnvironment` returns `null` instead of silently falling back to the default environment <img width="2580" height="1882" alt="CleanShot 2026-03-17 at 15 18 37@2x" src="https://github.com/user-attachments/assets/dfc7ea5e-1cfe-4305-a12d-94ece94e3939" /> <img width="2526" height="1792" alt="CleanShot 2026-03-17 at 15 17 58@2x" src="https://github.com/user-attachments/assets/b4112ba8-8499-46ab-9dcd-40a668e474e6" /> Fixes AGE-1604 ## Test plan - [x] Navigate to an external MCP server with no `defaultEnvironmentId` set - [x] Verify no green/yellow dot appears on any environment tab - [x] Verify the "Attach" button appears on all environment tabs - [x] Verify environment variables are editable on all tabs - [x] Navigate to an MCP server WITH a `defaultEnvironmentId` set - [x] Verify the attached environment still shows the green/yellow dot - [x] Verify the "Attach" button appears on non-attached environments - [x] Verify saving/attaching an environment still works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/speakeasy-api/gram/pull/1898" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This change updates the auth service (Authenticate method) to log details about each auth check, including the auth schemes that were used. <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/speakeasy-api/gram/pull/1908" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
…efix (#1909) - Renames API field from `toolset_id` to `toolset_slug` on the observability overview endpoint. - Switches ClickHouse queries from `startsWith(gram_urn, '<prefix>:')` to `toolset_slug = ?`, using the materialized column added in #1906. - Simplifies the dashboard — passes `selectedMcpServer` (already the toolset slug) directly instead of extracting a URN prefix from `toolUrns[0]`. - Regenerates the TypeScript SDK to match. Follows #1903 (telemetry attribute recording) and #1906 (ClickHouse materialized column). This completes the end-to-end flow for filtering observability data by MCP server name. <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/speakeasy-api/gram/pull/1909" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 1ebef8a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| var serverDetails *ServerDetails | ||
| isInternal := registry.Source.Valid && registry.Source.String == string(types.RegistrySourceInternal) | ||
| if isInternal { | ||
| remoteURL := fmt.Sprintf("%s/mcp/%s", te.serverURL.String(), task.MCP.RegistryServerSpecifier) |
There was a problem hiding this comment.
🔴 Nil pointer dereference on te.serverURL.String() when processing internal registries
When processing a deployment that contains an internal MCP registry, te.serverURL.String() is called at line 113 without a nil guard. The serverURL field can be nil — notably, ForDeploymentProcessing() at server/internal/background/worker.go:78 explicitly sets ServerURL: nil, and this is used by all test setups (deployments/setup_test.go:78, toolsets/setup_test.go:93, functions/setup_test.go:87, etc.). If any test deploys with an internal registry, the Temporal activity will panic with a nil pointer dereference. Even in production, there's no validation that serverURL is non-nil before use.
Prompt for agents
In server/internal/externalmcp/process.go, add a nil check for te.serverURL before dereferencing it on line 113. If te.serverURL is nil when isInternal is true, return an error like: return oops.E(oops.CodeInvariantViolation, nil, "[%s] serverURL is required for internal registries", task.MCP.Name).Log(ctx, logger). Alternatively, add a guard at the top of the Do() method that returns early if te.serverURL is nil and the registry is internal. Also consider updating ForDeploymentProcessing() in server/internal/background/worker.go to accept a serverURL parameter so tests can provide a non-nil value.
Was this helpful? React with 👍 or 👎 to provide feedback.
| toolsetSlug: toolset.slug, | ||
| defaultEnvironmentId: | ||
| mcpMetadata?.defaultEnvironmentId || targetEnv.id, | ||
| defaultEnvironmentId: mcpMetadata?.defaultEnvironmentId, |
There was a problem hiding this comment.
🚩 handleSaveAll no longer auto-attaches environment on first save
The old code at MCPEnvironmentSettings.tsx:576-577 was defaultEnvironmentId: mcpMetadata?.defaultEnvironmentId || targetEnv.id, which would auto-attach the target environment on first save when no environment was previously attached. The new code passes defaultEnvironmentId: mcpMetadata?.defaultEnvironmentId (which is undefined on first save). This is the intentional fix described in the changeset — the old behavior incorrectly showed a default as attached when none was explicitly attached. However, this means first-time saves will persist environment configs without attaching any environment. Users must now explicitly click "Attach" to link an environment. Verify this UX flow is acceptable and that the backend handles defaultEnvironmentId: undefined gracefully.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const { data: registriesData, isLoading: registriesLoading } = useQuery({ | ||
| queryKey: ["collections", "list"], | ||
| queryFn: () => client.mcpRegistries.listRegistries({}), | ||
| }); |
There was a problem hiding this comment.
🚩 useCatalogCollections query key differs from useCollections, risking stale cache across projects
useCatalogCollections in client/dashboard/src/pages/catalog/hooks.ts:128-131 uses queryKey: ["collections", "list"] and calls listRegistries({}) without gramProject. In contrast, useCollections in client/dashboard/src/pages/collections/hooks.ts:67-69 uses queryKey: ["collections", "list", projectSlug] and passes gramProject. Since the Catalog page is project-scoped, the SDK client likely auto-includes the project header. However, the query key lacks project scoping, so navigating between projects could return cached data from a different project. This may be harmless if listRegistries returns org-level data (same across projects), but deserves verification.
Was this helpful? React with 👍 or 👎 to provide feedback.
| func (s *Auth) logAuthContext(ctx context.Context, err error, scheme string) { | ||
| authCtx, ok := contextvalues.GetAuthContext(ctx) | ||
| if !ok { | ||
| return | ||
| } | ||
|
|
||
| attrs := []any{ | ||
| attr.SlogAuthScheme(scheme), | ||
| attr.SlogAuthOrganizationID(authCtx.ActiveOrganizationID), | ||
| attr.SlogAuthOrganizationSlug(authCtx.OrganizationSlug), | ||
| attr.SlogAuthAccountType(authCtx.AccountType), | ||
| } | ||
| if err != nil { | ||
| attrs = append(attrs, attr.SlogError(err)) | ||
| } | ||
| if authCtx.UserID != "" { | ||
| attrs = append(attrs, attr.SlogAuthUserID(authCtx.UserID)) | ||
| } | ||
| if authCtx.ExternalUserID != "" { | ||
| attrs = append(attrs, attr.SlogAuthUserExternalID(authCtx.ExternalUserID)) | ||
| } | ||
| if authCtx.Email != nil { | ||
| attrs = append(attrs, attr.SlogAuthUserEmail(*authCtx.Email)) | ||
| } | ||
| if authCtx.APIKeyID != "" { | ||
| attrs = append(attrs, attr.SlogAuthAPIKeyID(authCtx.APIKeyID)) | ||
| } | ||
| if authCtx.SessionID != nil { | ||
| attrs = append(attrs, attr.SlogAuthSessionID(*authCtx.SessionID)) | ||
| } | ||
| if authCtx.ProjectID != nil { | ||
| attrs = append(attrs, attr.SlogAuthProjectID(authCtx.ProjectID.String())) | ||
| } | ||
| if authCtx.ProjectSlug != nil { | ||
| attrs = append(attrs, attr.SlogAuthProjectSlug(*authCtx.ProjectSlug)) | ||
| } | ||
|
|
||
| if err != nil { | ||
| s.logger.ErrorContext(ctx, fmt.Sprintf("auth scheme check failed (%s)", scheme), attrs...) | ||
| } else { | ||
| s.logger.InfoContext(ctx, fmt.Sprintf("auth scheme check passed (%s)", scheme), attrs...) | ||
| } | ||
| } |
There was a problem hiding this comment.
🚩 Auth logging runs on every scheme check including ProjectSlug, producing verbose logs
The new logAuthContext method in server/internal/auth/auth.go:134-176 is called via defer at line 47 on every Authorize call. Since Goa calls Authorize for each security scheme in the chain (e.g. Session, then ProjectSlug), this will produce two log lines per authenticated request — one for the session check and one for the project slug check. At INFO level this could be very verbose in production. Consider whether this should be DEBUG level, or only logged on errors.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
add-registries-ryMYto pick up new registry processing commitsTest plan
🤖 Generated with Claude Code