Skip to content

feat: Add catalog tabs, flatten MCP overview, simplify collection install#1925

Open
qstearns wants to merge 21 commits intofeat/collections-templatesfrom
collections-t5Eu
Open

feat: Add catalog tabs, flatten MCP overview, simplify collection install#1925
qstearns wants to merge 21 commits intofeat/collections-templatesfrom
collections-t5Eu

Conversation

@qstearns
Copy link
Copy Markdown
Contributor

@qstearns qstearns commented Mar 19, 2026

Summary

  • Add "My Organization" and "Discover" tabbed navigation to the project-level Catalog page, bringing collections into the catalog UI
  • Remove grouped/stacked collection card rendering on the MCP overview page in favor of flat individual server cards
  • Simplify the collection detail install action from a split-button dropdown (Install / Slack App) to a single Install button
  • Rebase onto latest add-registries-ryMY to pick up new registry processing commits

Test plan

  • Navigate to project Catalog page and verify "My Organization" and "Discover" tabs appear
  • Verify "My Organization" tab shows internal collections with search
  • Verify "Discover" tab shows external registry servers with full filter/sort controls
  • Navigate to MCP overview and verify collection servers render as flat cards (no grouped/stacked UI)
  • Open a collection detail page and verify single "Install" button (no dropdown)
  • Install a collection and verify servers are added correctly

🤖 Generated with Claude Code


Open with Devin

qstearns and others added 21 commits March 18, 2026 11:16
- 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>
@qstearns qstearns requested a review from a team as a code owner March 19, 2026 14:15
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 1ebef8a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
dashboard Patch

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

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment Mar 19, 2026 2:15pm

Request Review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

toolsetSlug: toolset.slug,
defaultEnvironmentId:
mcpMetadata?.defaultEnvironmentId || targetEnv.id,
defaultEnvironmentId: mcpMetadata?.defaultEnvironmentId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +128 to +131
const { data: registriesData, isLoading: registriesLoading } = useQuery({
queryKey: ["collections", "list"],
queryFn: () => client.mcpRegistries.listRegistries({}),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +134 to +176
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...)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@simplesagar simplesagar changed the title Add catalog tabs, flatten MCP overview, simplify collection install feat: Add catalog tabs, flatten MCP overview, simplify collection install Mar 23, 2026
@simplesagar simplesagar requested a review from adaam2 March 23, 2026 10:28
@simplesagar simplesagar added automation preview Spawn a preview environment and removed automation labels Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Spawn a preview environment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants