Skip to content

Commit 709bf5b

Browse files
authored
feat: add MCP server for AI agent tool access (#248)
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.
1 parent af1ccdd commit 709bf5b

File tree

12 files changed

+1016
-4
lines changed

12 files changed

+1016
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ vendor/
2727
/config.yaml
2828
compass.yaml
2929
temp/
30+
.mcp.json

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ services:
1515
postgres:
1616
image: postgres:13
1717
ports:
18-
- 5432:5432
18+
- 5433:5432
1919
environment:
2020
POSTGRES_USER: compass
2121
POSTGRES_PASSWORD: compass_password

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ require (
2727
github.com/jmoiron/sqlx v1.4.0
2828
github.com/lestrrat-go/jwx/v2 v2.1.6
2929
github.com/lib/pq v1.12.0
30+
github.com/mark3labs/mcp-go v0.46.0
3031
github.com/ory/dockertest/v3 v3.12.0
3132
github.com/peterbourgon/mergemap v0.0.1
3233
github.com/r3labs/diff/v3 v3.0.2
@@ -135,6 +136,7 @@ require (
135136
github.com/go-playground/validator v9.31.0+incompatible // indirect
136137
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
137138
github.com/google/cel-go v0.26.1 // indirect
139+
github.com/google/jsonschema-go v0.4.2 // indirect
138140
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
139141
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
140142
github.com/jackc/puddle/v2 v2.2.2 // indirect
@@ -145,12 +147,13 @@ require (
145147
github.com/sagikazarmark/locafero v0.4.0 // indirect
146148
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
147149
github.com/sourcegraph/conc v0.3.0 // indirect
148-
github.com/spf13/cast v1.6.0 // indirect
150+
github.com/spf13/cast v1.7.1 // indirect
149151
github.com/spf13/viper v1.19.0 // indirect
150152
github.com/stoewer/go-strcase v1.3.1 // indirect
151153
github.com/subosito/gotenv v1.6.0 // indirect
152154
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
153155
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
156+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
154157
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
155158
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
156159
go.opentelemetry.io/otel/metric v1.42.0 // indirect

go.sum

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
139139
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
140140
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
141141
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
142+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
143+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
142144
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
143145
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
144146
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -201,6 +203,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
201203
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
202204
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
203205
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
206+
github.com/mark3labs/mcp-go v0.46.0 h1:8KRibF4wcKejbLsHxCA/QBVUr5fQ9nwz/n8lGqmaALo=
207+
github.com/mark3labs/mcp-go v0.46.0/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag=
204208
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
205209
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
206210
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -290,8 +294,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS
290294
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
291295
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
292296
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
293-
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
294-
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
297+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
298+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
295299
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
296300
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
297301
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
@@ -329,6 +333,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
329333
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
330334
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
331335
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
336+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
337+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
332338
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
333339
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
334340
github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=

internal/mcp/format.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package mcp
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/raystack/compass/core/asset"
8+
)
9+
10+
// formatAsset formats an asset as LLM-friendly markdown text.
11+
func formatAsset(a asset.Asset) string {
12+
var b strings.Builder
13+
14+
fmt.Fprintf(&b, "## %s (%s)\n", a.Name, a.Type)
15+
fmt.Fprintf(&b, "Service: %s | URN: %s\n", a.Service, a.URN)
16+
17+
if a.Description != "" {
18+
fmt.Fprintf(&b, "Description: %s\n", a.Description)
19+
}
20+
21+
if len(a.Owners) > 0 {
22+
names := make([]string, 0, len(a.Owners))
23+
for _, o := range a.Owners {
24+
if o.Email != "" {
25+
names = append(names, o.Email)
26+
} else {
27+
names = append(names, o.UUID)
28+
}
29+
}
30+
fmt.Fprintf(&b, "Owners: %s\n", strings.Join(names, ", "))
31+
}
32+
33+
if a.URL != "" {
34+
fmt.Fprintf(&b, "URL: %s\n", a.URL)
35+
}
36+
37+
if len(a.Labels) > 0 {
38+
pairs := make([]string, 0, len(a.Labels))
39+
for k, v := range a.Labels {
40+
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
41+
}
42+
fmt.Fprintf(&b, "Labels: %s\n", strings.Join(pairs, ", "))
43+
}
44+
45+
formatAssetData(&b, a.Data)
46+
47+
return b.String()
48+
}
49+
50+
// formatAssetData formats the Data map, extracting schema columns if present.
51+
func formatAssetData(b *strings.Builder, data map[string]interface{}) {
52+
if data == nil {
53+
return
54+
}
55+
56+
// Extract schema/columns if present (common in table/topic assets)
57+
if columns, ok := extractColumns(data); ok && len(columns) > 0 {
58+
fmt.Fprintf(b, "\nColumns (%d):\n", len(columns))
59+
for _, col := range columns {
60+
name, _ := col["name"].(string)
61+
dataType, _ := col["data_type"].(string)
62+
desc, _ := col["description"].(string)
63+
64+
if desc != "" {
65+
fmt.Fprintf(b, " - %s (%s): %s\n", name, dataType, desc)
66+
} else {
67+
fmt.Fprintf(b, " - %s (%s)\n", name, dataType)
68+
}
69+
}
70+
}
71+
}
72+
73+
// extractColumns tries to find column definitions in asset data.
74+
func extractColumns(data map[string]interface{}) ([]map[string]interface{}, bool) {
75+
// Try common paths: data.columns, data.schema.columns
76+
if cols, ok := data["columns"]; ok {
77+
return toMapSlice(cols)
78+
}
79+
if schema, ok := data["schema"].(map[string]interface{}); ok {
80+
if cols, ok := schema["columns"]; ok {
81+
return toMapSlice(cols)
82+
}
83+
}
84+
return nil, false
85+
}
86+
87+
func toMapSlice(v interface{}) ([]map[string]interface{}, bool) {
88+
slice, ok := v.([]interface{})
89+
if !ok {
90+
return nil, false
91+
}
92+
result := make([]map[string]interface{}, 0, len(slice))
93+
for _, item := range slice {
94+
if m, ok := item.(map[string]interface{}); ok {
95+
result = append(result, m)
96+
}
97+
}
98+
return result, len(result) > 0
99+
}
100+
101+
// formatSearchResult formats a search result as a compact line.
102+
func formatSearchResult(sr asset.SearchResult) string {
103+
var b strings.Builder
104+
fmt.Fprintf(&b, "- **%s** (%s) — service: %s, urn: %s", sr.Title, sr.Type, sr.Service, sr.URN)
105+
if sr.Description != "" {
106+
desc := sr.Description
107+
if len(desc) > 120 {
108+
desc = desc[:120] + "..."
109+
}
110+
fmt.Fprintf(&b, "\n %s", desc)
111+
}
112+
return b.String()
113+
}
114+
115+
// formatSearchResults formats a list of search results.
116+
func formatSearchResults(results []asset.SearchResult) string {
117+
if len(results) == 0 {
118+
return "No assets found."
119+
}
120+
121+
var b strings.Builder
122+
fmt.Fprintf(&b, "Found %d assets:\n\n", len(results))
123+
for _, sr := range results {
124+
b.WriteString(formatSearchResult(sr))
125+
b.WriteString("\n")
126+
}
127+
return b.String()
128+
}
129+
130+
// formatLineage formats lineage data as readable text.
131+
func formatLineage(urn string, lineage asset.Lineage) string {
132+
if len(lineage.Edges) == 0 {
133+
return fmt.Sprintf("No lineage found for %s.", urn)
134+
}
135+
136+
var b strings.Builder
137+
fmt.Fprintf(&b, "Lineage for %s (%d edges):\n\n", urn, len(lineage.Edges))
138+
139+
upstreams := make([]string, 0)
140+
downstreams := make([]string, 0)
141+
142+
for _, edge := range lineage.Edges {
143+
if edge.Target == urn {
144+
upstreams = append(upstreams, edge.Source)
145+
} else if edge.Source == urn {
146+
downstreams = append(downstreams, edge.Target)
147+
} else {
148+
// Transitive edges
149+
fmt.Fprintf(&b, " %s → %s\n", edge.Source, edge.Target)
150+
}
151+
}
152+
153+
if len(upstreams) > 0 {
154+
b.WriteString("Upstream (sources):\n")
155+
for _, u := range upstreams {
156+
fmt.Fprintf(&b, " ← %s\n", u)
157+
}
158+
}
159+
160+
if len(downstreams) > 0 {
161+
b.WriteString("Downstream (consumers):\n")
162+
for _, d := range downstreams {
163+
fmt.Fprintf(&b, " → %s\n", d)
164+
}
165+
}
166+
167+
return b.String()
168+
}
169+
170+
// formatTypes formats asset type counts.
171+
func formatTypes(types map[asset.Type]int) string {
172+
if len(types) == 0 {
173+
return "No asset types found."
174+
}
175+
176+
var b strings.Builder
177+
b.WriteString("Asset types:\n\n")
178+
for t, count := range types {
179+
fmt.Fprintf(&b, "- %s: %d assets\n", t, count)
180+
}
181+
return b.String()
182+
}
183+
184+
// formatAssets formats a list of assets as a summary list.
185+
func formatAssets(assets []asset.Asset, total uint32) string {
186+
if len(assets) == 0 {
187+
return "No assets found."
188+
}
189+
190+
var b strings.Builder
191+
if total > 0 {
192+
fmt.Fprintf(&b, "Showing %d of %d assets:\n\n", len(assets), total)
193+
} else {
194+
fmt.Fprintf(&b, "Found %d assets:\n\n", len(assets))
195+
}
196+
197+
for _, a := range assets {
198+
fmt.Fprintf(&b, "- **%s** (%s) — service: %s, urn: %s\n", a.Name, a.Type, a.Service, a.URN)
199+
}
200+
return b.String()
201+
}

0 commit comments

Comments
 (0)