Skip to content

Commit de41225

Browse files
DavidS-ovmactions-user
authored andcommitted
feat(customermcp): Phase 3 — Infrastructure Context Resources and Inventory Tools (#4597)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Implements Phase 3 of the Customer-Facing MCP Server. This phase adds infrastructure context — sources and snapshots — enabling AI agents to understand what infrastructure is connected, what resource types are discoverable, and what snapshots exist. ## What changed ### New Tools (4) | Tool | Scope | Description | | --- | --- | --- | | `list_sources` | `sources:read` | List all connected sources with optional `status` and `type` filters | | `get_source_status` | `sources:read` | Get detailed status for a specific source by UUID | | `list_snapshots` | `changes:read` | List all infrastructure snapshots with summary counts | | `get_snapshot` | `changes:read` | Get snapshot details; summary by default, full items with `include_items=true` | ### New Resources (2) | Resource | Scope | Description | | --- | --- | --- | | `sources://list` | `sources:read` | Static list of all connected sources | | `sources://{source_id}/types` | `sources:read` | Resource template: available types for a specific source | ### Architecture patterns established 1. **MCP ResourceTemplate** — First use of `server.AddResourceTemplate` with RFC 6570 URI templates and scope-enforced `addResourceTemplate` wrapper 2. **Cross-service gateway calls** — `SnapshotsServiceClient` created once at startup with `ContextAwareAuthTransport` for per-request JWT passthrough to the gateway 3. **sdp-go interface reuse** — `ManagementServiceHandler` and `SnapshotsServiceClient` used directly as deps (no custom interfaces), following the updated convention from the plan ### Files changed | File | Change | | --- | --- | | `go/auth/auth_client.go` | Add `ContextAwareAuthTransport` and `NewContextAwareAuthClient` for per-request JWT extraction | | `go/auth/context_aware_auth_test.go` | Unit tests for the new transport | | `services/api-server/customermcp/deps.go` | Add `SourcesService` and `SnapshotsService` fields to `Deps` | | `services/api-server/customermcp/sources.go` | `list_sources` and `get_source_status` tool handlers | | `services/api-server/customermcp/snapshots.go` | `list_snapshots` and `get_snapshot` tool handlers | | `services/api-server/customermcp/resources.go` | `sources://list` resource and `sources://{source_id}/types` template | | `services/api-server/customermcp/scope.go` | `addResourceTemplate` scope wrapper | | `services/api-server/customermcp/tools.go` | Register all 4 new tools | | `services/api-server/customermcp/format.go` | `formatSourceStatus` and `formatSourceManaged` helpers | | `services/api-server/customermcp/sources_test.go` | Comprehensive tests for source tools and resources | | `services/api-server/customermcp/snapshots_test.go` | Comprehensive tests for snapshot tools | | `services/api-server/customermcp/changes_test.go` | Update test deps for new tool registration | | `services/api-server/customermcp/mcp_test.go` | Update test deps for new tool registration | | `services/api-server/service/main.go` | Add `ManagementService()` accessor | | `services/api-server/cmd/start.go` | Wire `SourcesService` and `SnapshotsService` after Init() | ## Testing - 43 new tests covering all tool handlers, resource handlers, scope enforcement, format helpers, registration, error handling, and the context-aware auth transport - All existing Phase 1/2 tests continue to pass (40+ tests) - Full compilation verified across `api-server`, `auth`, and `customermcp` packages <!-- CURSOR_AGENT_PR_BODY_END --> Linear Issue: [ENG-3579](https://linear.app/overmind/issue/ENG-3579/phase-3-customer-mcp-infrastructure-context-resources-and-inventory) <div><a href="https://cursor.com/agents/bc-56e4e223-5d42-425f-8aab-4f88772ddd27"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-56e4e223-5d42-425f-8aab-4f88772ddd27"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> GitOrigin-RevId: 47728085e9b6275fd49ce451ff683e2e3a8f1c6b
1 parent 0815f54 commit de41225

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

go/auth/auth_client.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,42 @@ func NewAuthenticatedClient(ctx context.Context, from *http.Client) *http.Client
5252
}
5353
}
5454

55+
// ContextAwareAuthTransport is an http.RoundTripper that extracts the user JWT
56+
// from each request's context at call time (not at client-creation time). This
57+
// enables a single persistent http.Client to pass through per-request JWTs,
58+
// which is needed when the client is created once at startup but serves
59+
// requests from different users.
60+
type ContextAwareAuthTransport struct {
61+
from http.RoundTripper
62+
}
63+
64+
// RoundTrip extracts the JWT from the request's context and adds it as a
65+
// Bearer token in the Authorization header.
66+
func (t *ContextAwareAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
67+
req.Header.Set("X-Overmind-Interactive", "false")
68+
69+
if token, ok := req.Context().Value(UserTokenContextKey{}).(string); ok && token != "" {
70+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
71+
}
72+
73+
return t.from.RoundTrip(req)
74+
}
75+
76+
// NewContextAwareAuthClient creates an http.Client whose transport extracts the
77+
// JWT from each outgoing request's context. Unlike NewAuthenticatedClient (which
78+
// captures the token once), this client re-reads the token on every call —
79+
// making it safe to reuse across requests from different users.
80+
func NewContextAwareAuthClient(from *http.Client) *http.Client {
81+
return &http.Client{
82+
Transport: &ContextAwareAuthTransport{
83+
from: from.Transport,
84+
},
85+
CheckRedirect: from.CheckRedirect,
86+
Jar: from.Jar,
87+
Timeout: from.Timeout,
88+
}
89+
}
90+
5591
// AuthenticatedAdminClient Returns a bookmark client that uses the auth
5692
// embedded in the context and otel instrumentation
5793
func AuthenticatedAdminClient(ctx context.Context, apiUrl string) sdpconnect.AdminServiceClient {

go/auth/context_aware_auth_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
)
9+
10+
func TestContextAwareAuthTransport_InjectsToken(t *testing.T) {
11+
var capturedAuth string
12+
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
13+
capturedAuth = r.Header.Get("Authorization")
14+
}))
15+
defer ts.Close()
16+
17+
client := NewContextAwareAuthClient(ts.Client())
18+
19+
ctx := context.WithValue(context.Background(), UserTokenContextKey{}, "test-jwt-token")
20+
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
21+
resp, err := client.Do(req)
22+
if err != nil {
23+
t.Fatalf("unexpected error: %v", err)
24+
}
25+
defer resp.Body.Close()
26+
27+
if capturedAuth != "Bearer test-jwt-token" {
28+
t.Errorf("expected 'Bearer test-jwt-token', got %q", capturedAuth)
29+
}
30+
}
31+
32+
func TestContextAwareAuthTransport_NoToken(t *testing.T) {
33+
var capturedAuth string
34+
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
35+
capturedAuth = r.Header.Get("Authorization")
36+
}))
37+
defer ts.Close()
38+
39+
client := NewContextAwareAuthClient(ts.Client())
40+
41+
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, ts.URL, nil)
42+
resp, err := client.Do(req)
43+
if err != nil {
44+
t.Fatalf("unexpected error: %v", err)
45+
}
46+
defer resp.Body.Close()
47+
48+
if capturedAuth != "" {
49+
t.Errorf("expected empty auth header, got %q", capturedAuth)
50+
}
51+
}
52+
53+
func TestContextAwareAuthTransport_DifferentTokensPerRequest(t *testing.T) {
54+
var capturedTokens []string
55+
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
56+
capturedTokens = append(capturedTokens, r.Header.Get("Authorization"))
57+
}))
58+
defer ts.Close()
59+
60+
client := NewContextAwareAuthClient(ts.Client())
61+
62+
for _, token := range []string{"token-a", "token-b"} {
63+
ctx := context.WithValue(context.Background(), UserTokenContextKey{}, token)
64+
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
65+
resp, err := client.Do(req)
66+
if err != nil {
67+
t.Fatalf("unexpected error: %v", err)
68+
}
69+
resp.Body.Close()
70+
}
71+
72+
if len(capturedTokens) != 2 {
73+
t.Fatalf("expected 2 requests, got %d", len(capturedTokens))
74+
}
75+
if capturedTokens[0] != "Bearer token-a" {
76+
t.Errorf("first request: expected 'Bearer token-a', got %q", capturedTokens[0])
77+
}
78+
if capturedTokens[1] != "Bearer token-b" {
79+
t.Errorf("second request: expected 'Bearer token-b', got %q", capturedTokens[1])
80+
}
81+
}

0 commit comments

Comments
 (0)