Skip to content

feat: add MCP server for AI agent tool access#248

Merged
ravisuhag merged 1 commit intomainfrom
feat/mcp-server
Mar 28, 2026
Merged

feat: add MCP server for AI agent tool access#248
ravisuhag merged 1 commit intomainfrom
feat/mcp-server

Conversation

@ravisuhag
Copy link
Copy Markdown
Member

Summary

  • Add Model Context Protocol (MCP) server integration to Compass, exposing asset search, retrieval, lineage, and type listing as MCP tools at the /mcp endpoint
  • Enable AI coding tools (Claude Code, Cursor, etc.) to discover and query Compass assets via the standard MCP protocol
  • Add .mcp.json to .gitignore and update docker-compose postgres port to avoid conflicts

Test plan

  • Verify MCP endpoint responds at /mcp when Compass server is running
  • Test asset search, get asset, get lineage, and list types tools via an MCP client
  • Confirm existing gRPC and REST endpoints are unaffected
  • Run unit tests in internal/mcp/

Add Model Context Protocol (MCP) server integration to Compass, enabling
AI coding tools to discover and query assets via the /mcp endpoint.
Also adds .mcp.json to .gitignore and updates docker-compose postgres port.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

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

Project Deployment Actions Updated (UTC)
compass Ready Ready Preview, Comment Mar 28, 2026 7:35pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

This change adds a new Model Context Protocol (MCP) server implementation to expose the catalog as AI-agent tools. A new internal/mcp package includes tool definitions for asset search, retrieval, lineage exploration, type listing, and bulk asset access. Handler functions parse incoming tool requests, invoke corresponding asset service methods, and return formatted results optimized for language models. The implementation integrates into the existing HTTP server, mounting an MCP endpoint at /mcp during startup, with complete test coverage for handlers and end-to-end flows.

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Server as MCP Server
    participant Handler as Tool Handler
    participant Service as Asset Service
    participant Formatter as Format Helper

    Client->>Server: CallTool(search_assets, {text, types, services})
    Server->>Handler: handleSearchAssets(request)
    Handler->>Handler: Parse & validate parameters
    Handler->>Service: SearchAssets(ctx, SearchConfig)
    Service-->>Handler: []SearchResult or error
    alt Service Success
        Handler->>Formatter: formatSearchResults(results)
        Formatter-->>Handler: Formatted markdown text
        Handler-->>Server: ToolResultText(content)
    else Service Error
        Handler-->>Server: ToolResultError(error message)
    end
    Server-->>Client: CallToolResult
Loading

Possibly Related Issues

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main feature: adding an MCP server for AI agent tool access, which matches the core objective of the changeset.
Description check ✅ Passed The description clearly outlines the MCP server integration, asset exposure via tools, gitignore addition, and docker-compose updates—all matching the implemented changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ravisuhag ravisuhag linked an issue Mar 28, 2026 that may be closed by this pull request
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
docker-compose.yaml (1)

18-18: Port change may require documentation update.

Changing the host port from 5432 to 5433 will require existing developers to update their local configuration. Consider adding a note in the PR description or README about this change to prevent confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yaml` at line 18, You changed the Docker Compose port mapping
from "5432" to "5433" (line "- 5433:5432") which affects local developer setups;
update the README/CONTRIBUTING and the PR description to note the new host port,
include the exact mapping "5433:5432", list steps to adjust local configs (e.g.,
DB clients or env vars pointing to localhost:5432 -> localhost:5433), and
mention how to revert or run on the old port if needed.
internal/mcp/handlers.go (1)

98-105: Consider adding an upper bound for size parameter.

The handler accepts any positive value for size without an upper limit. A malicious or mistaken request with a very large size could strain backend resources. Consider capping it to a reasonable maximum (e.g., 100 or 500).

Proposed fix
 func (s *Server) handleGetAllAssets(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	size := mcp.ParseInt(req, "size", 20)
+	if size > 500 {
+		size = 500
+	}
 	offset := mcp.ParseInt(req, "offset", 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/handlers.go` around lines 98 - 105, The handler
handleGetAllAssets allows an unbounded size from mcp.ParseInt which can be
abused; update the code that computes size (variable size in handleGetAllAssets)
to enforce a maximum (e.g., const maxSize = 100) and clamp/validate the parsed
value (ensure non-negative and if size > maxSize set size = maxSize) before
passing it into asset.NewFilterBuilder().Size(size); keep the default behavior
when ParseInt yields no value and add a brief comment about the cap.
internal/mcp/format.go (1)

59-68: Consider handling empty column names more gracefully.

When name or data_type are missing or not strings, the output could produce entries like "- ()". While this defensive approach is acceptable for unstructured data, you might consider skipping columns without a name.

Optional improvement
 		for _, col := range columns {
 			name, _ := col["name"].(string)
+			if name == "" {
+				continue
+			}
 			dataType, _ := col["data_type"].(string)
 			desc, _ := col["description"].(string)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/format.go` around lines 59 - 68, Skip rendering columns that
lack a valid non-empty name: inside the loop that iterates over columns
(variables name, dataType, desc and uses fmt.Fprintf to write to buffer b),
first validate that name is a non-empty string and if not continue to the next
column; for dataType you can default to a placeholder like "unknown" when it's
missing or not a string before calling fmt.Fprintf so you don't emit entries
like "- ()".
internal/mcp/handlers_test.go (2)

112-117: Avoid brittle direct indexing in filter assertions.

Lines 112-117 directly index map/slice entries; when mapping breaks, this can panic instead of producing clear assertion output.

🔧 Safer assertions
-				if cfg.Filters["type"][0] != "table" {
-					t.Errorf("expected type filter 'table', got %v", cfg.Filters["type"])
-				}
-				if cfg.Filters["service"][0] != "bigquery" {
-					t.Errorf("expected service filter 'bigquery', got %v", cfg.Filters["service"])
-				}
+				gotTypes := cfg.Filters["type"]
+				if len(gotTypes) != 1 || gotTypes[0] != "table" {
+					t.Errorf("expected type filter ['table'], got %v", gotTypes)
+				}
+				gotServices := cfg.Filters["service"]
+				if len(gotServices) != 1 || gotServices[0] != "bigquery" {
+					t.Errorf("expected service filter ['bigquery'], got %v", gotServices)
+				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/handlers_test.go` around lines 112 - 117, The assertions in the
test use brittle direct indexing (cfg.Filters["type"][0] and
cfg.Filters["service"][0]) which can panic; update the checks on cfg and its
Filters to first assert the key exists and the slice has expected length, then
assert the slice contains the expected value (e.g., check ok :=
cfg.Filters["type"]; if !ok || len(ok) == 0 { t.Fatalf(...) } else if ok[0] !=
"table" { t.Errorf(...) }) or use a safe helper/assertion (assert.Contains or
reflect.DeepEqual) to compare the slices without direct indexing; apply the same
pattern for "service" to avoid panics and provide clear failure messages.

165-166: Assert handler argument passthrough in mocks.

Line 165 (id) and Line 205 (urn) are not validated in the service doubles. Adding explicit checks will catch request parsing/mapping regressions earlier.

Also applies to: 205-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/handlers_test.go` around lines 165 - 166, In the test doubles
(e.g., getAssetByIDFunc and the analogous getAssetByURNFunc/mock), add explicit
assertions on the incoming handler arguments by comparing the received id/urn to
the expected test value and returning an error if they don't match; update the
mock implementations (getAssetByIDFunc and the mock for URN lookup) to validate
the argument before constructing the asset so request parsing/mapping
regressions are caught early.
internal/mcp/server.go (1)

30-34: Fail fast on invalid constructor dependencies.

Line 30 currently accepts nil dependencies silently; a miswired bootstrap can fail later at request time. Add early validation in New for clearer startup-time failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/server.go` around lines 30 - 34, Modify the constructor New to
fail fast by changing its signature to return (*Server, error), validate that
the incoming assetSvc (AssetService) and ns (*namespace.Namespace) are non-nil,
and return a descriptive error (e.g., fmt.Errorf("New: nil AssetService") or
fmt.Errorf("New: nil namespace.Namespace")) if either is nil; on success return
the constructed *Server and nil error. Update all call sites to handle the new
(*Server, error) return and propagate or handle the error at bootstrap time so
miswired dependencies fail at startup rather than at request time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/mcp/server_test.go`:
- Around line 25-42: The mock getAssetByIDFunc currently ignores its id argument
which hides request-mapping regressions; update getAssetByIDFunc (and the
similar mock used around lines 138-143) to explicitly assert the incoming id
equals the expected URN (e.g., "urn:bq:dataset.orders" or the test's expectedID
variable) and make the mock fail the test (or return a distinct error) if it
doesn't match so the e2e test validates that GetAssetByID is called with the
URN, not some other identifier.

---

Nitpick comments:
In `@docker-compose.yaml`:
- Line 18: You changed the Docker Compose port mapping from "5432" to "5433"
(line "- 5433:5432") which affects local developer setups; update the
README/CONTRIBUTING and the PR description to note the new host port, include
the exact mapping "5433:5432", list steps to adjust local configs (e.g., DB
clients or env vars pointing to localhost:5432 -> localhost:5433), and mention
how to revert or run on the old port if needed.

In `@internal/mcp/format.go`:
- Around line 59-68: Skip rendering columns that lack a valid non-empty name:
inside the loop that iterates over columns (variables name, dataType, desc and
uses fmt.Fprintf to write to buffer b), first validate that name is a non-empty
string and if not continue to the next column; for dataType you can default to a
placeholder like "unknown" when it's missing or not a string before calling
fmt.Fprintf so you don't emit entries like "- ()".

In `@internal/mcp/handlers_test.go`:
- Around line 112-117: The assertions in the test use brittle direct indexing
(cfg.Filters["type"][0] and cfg.Filters["service"][0]) which can panic; update
the checks on cfg and its Filters to first assert the key exists and the slice
has expected length, then assert the slice contains the expected value (e.g.,
check ok := cfg.Filters["type"]; if !ok || len(ok) == 0 { t.Fatalf(...) } else
if ok[0] != "table" { t.Errorf(...) }) or use a safe helper/assertion
(assert.Contains or reflect.DeepEqual) to compare the slices without direct
indexing; apply the same pattern for "service" to avoid panics and provide clear
failure messages.
- Around line 165-166: In the test doubles (e.g., getAssetByIDFunc and the
analogous getAssetByURNFunc/mock), add explicit assertions on the incoming
handler arguments by comparing the received id/urn to the expected test value
and returning an error if they don't match; update the mock implementations
(getAssetByIDFunc and the mock for URN lookup) to validate the argument before
constructing the asset so request parsing/mapping regressions are caught early.

In `@internal/mcp/handlers.go`:
- Around line 98-105: The handler handleGetAllAssets allows an unbounded size
from mcp.ParseInt which can be abused; update the code that computes size
(variable size in handleGetAllAssets) to enforce a maximum (e.g., const maxSize
= 100) and clamp/validate the parsed value (ensure non-negative and if size >
maxSize set size = maxSize) before passing it into
asset.NewFilterBuilder().Size(size); keep the default behavior when ParseInt
yields no value and add a brief comment about the cap.

In `@internal/mcp/server.go`:
- Around line 30-34: Modify the constructor New to fail fast by changing its
signature to return (*Server, error), validate that the incoming assetSvc
(AssetService) and ns (*namespace.Namespace) are non-nil, and return a
descriptive error (e.g., fmt.Errorf("New: nil AssetService") or fmt.Errorf("New:
nil namespace.Namespace")) if either is nil; on success return the constructed
*Server and nil error. Update all call sites to handle the new (*Server, error)
return and propagate or handle the error at bootstrap time so miswired
dependencies fail at startup rather than at request time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 91b10449-e6b8-4af1-85f2-ca9a6ffc50cc

📥 Commits

Reviewing files that changed from the base of the PR and between af1ccdd and e53103a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • .gitignore
  • docker-compose.yaml
  • go.mod
  • internal/mcp/format.go
  • internal/mcp/handlers.go
  • internal/mcp/handlers_test.go
  • internal/mcp/server.go
  • internal/mcp/server_test.go
  • internal/mcp/tools.go
  • internal/server/bootstrap.go
  • internal/server/server.go

Comment on lines +25 to +42
getAssetByIDFunc: func(_ context.Context, id string) (asset.Asset, error) {
return asset.Asset{
ID: "1",
URN: "urn:bq:dataset.orders",
Name: "orders",
Type: "table",
Service: "bigquery",
Description: "Main orders table with all customer transactions",
Owners: []user.User{{Email: "alice@company.com"}, {Email: "bob@company.com"}},
Data: map[string]interface{}{
"columns": []interface{}{
map[string]interface{}{"name": "order_id", "data_type": "INTEGER", "description": "Primary key"},
map[string]interface{}{"name": "customer_id", "data_type": "INTEGER", "description": "FK to customers"},
map[string]interface{}{"name": "amount", "data_type": "FLOAT", "description": "Order total in USD"},
},
},
}, nil
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten get_asset identifier contract in the e2e test.

Line 142 sends a URN in the id argument, and Lines 25-42 don’t assert what was passed into GetAssetByID. This can hide request-mapping regressions.

🔧 Proposed test hardening
 		getAssetByIDFunc: func(_ context.Context, id string) (asset.Asset, error) {
+			if id != "1" {
+				t.Errorf("expected id '1', got '%s'", id)
+			}
 			return asset.Asset{
 				ID:          "1",
 				URN:         "urn:bq:dataset.orders",
@@
 	t.Run("get_asset", func(t *testing.T) {
 		result, err := client.CallTool(ctx, mcp.CallToolRequest{
 			Params: mcp.CallToolParams{
 				Name:      "get_asset",
-				Arguments: map[string]any{"id": "urn:bq:dataset.orders"},
+				Arguments: map[string]any{"id": "1"},
 			},
 		})

Also applies to: 138-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/server_test.go` around lines 25 - 42, The mock getAssetByIDFunc
currently ignores its id argument which hides request-mapping regressions;
update getAssetByIDFunc (and the similar mock used around lines 138-143) to
explicitly assert the incoming id equals the expected URN (e.g.,
"urn:bq:dataset.orders" or the test's expectedID variable) and make the mock
fail the test (or return a distinct error) if it doesn't match so the e2e test
validates that GetAssetByID is called with the URN, not some other identifier.

@ravisuhag ravisuhag merged commit 709bf5b into main Mar 28, 2026
5 checks passed
@ravisuhag ravisuhag deleted the feat/mcp-server branch March 28, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add MCP server to expose catalog as AI-agent tools

1 participant