Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/contracts/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func ConvertServerConfig(cfg *config.ServerConfig, status string, connected bool
Updated: cfg.Updated,
ReconnectCount: 0, // TODO: Get from runtime status
Authenticated: authenticated,
// MCP-901: carry registry provenance so the approval/quarantine view
// can show a server's origin. Empty for manually-configured servers.
SourceRegistryID: cfg.SourceRegistryID,
SourceRegistryProvenance: cfg.SourceRegistryProvenance,
}

// Convert OAuth config if present
Expand Down Expand Up @@ -322,6 +326,15 @@ func ConvertGenericServersToTyped(genericServers []map[string]interface{}) []Ser
server.Diagnostic = d
}

// MCP-901 — registry provenance, carried through the legacy fallback
// projection in parity with the management.ListServers happy path.
if regID, ok := generic["source_registry_id"].(string); ok && regID != "" {
server.SourceRegistryID = regID
}
if prov, ok := generic["source_registry_provenance"].(string); ok && prov != "" {
server.SourceRegistryProvenance = prov
}

servers = append(servers, server)
}

Expand Down
53 changes: 53 additions & 0 deletions internal/contracts/converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"
"time"

"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -84,6 +85,58 @@ func TestConvertGenericServersToTyped_EmptyOAuth(t *testing.T) {
assert.Empty(t, servers[0].OAuth.ClientID)
}

// TestConvertGenericServersToTyped_SourceRegistry verifies registry provenance
// (MCP-901) is carried through the generic-map fallback projection so the
// approval/quarantine view can show a server's origin.
func TestConvertGenericServersToTyped_SourceRegistry(t *testing.T) {
genericServers := []map[string]interface{}{
{
"id": "everything",
"name": "everything",
"enabled": true,
"source_registry_id": "modelcontextprotocol",
"source_registry_provenance": "custom/unverified",
},
{
// Manually-configured server: both fields absent → empty.
"id": "manual",
"name": "manual",
"enabled": true,
},
}

servers := ConvertGenericServersToTyped(genericServers)
require.Len(t, servers, 2)

assert.Equal(t, "modelcontextprotocol", servers[0].SourceRegistryID)
assert.Equal(t, "custom/unverified", servers[0].SourceRegistryProvenance)

assert.Empty(t, servers[1].SourceRegistryID, "manual server carries no registry id")
assert.Empty(t, servers[1].SourceRegistryProvenance)
}

// TestConvertServerConfig_SourceRegistry verifies the direct config→contracts
// mapper populates registry provenance (MCP-901).
func TestConvertServerConfig_SourceRegistry(t *testing.T) {
cfg := &config.ServerConfig{
Name: "everything",
Protocol: "stdio",
Enabled: true,
SourceRegistryID: "modelcontextprotocol",
SourceRegistryProvenance: config.RegistryProvenanceCustom,
}

server := ConvertServerConfig(cfg, "ready", true, 3, false)
require.NotNil(t, server)
assert.Equal(t, "modelcontextprotocol", server.SourceRegistryID)
assert.Equal(t, config.RegistryProvenanceCustom, server.SourceRegistryProvenance)

// Manual server (no source registry) leaves both empty.
manual := ConvertServerConfig(&config.ServerConfig{Name: "manual", Enabled: true}, "ready", true, 0, false)
assert.Empty(t, manual.SourceRegistryID)
assert.Empty(t, manual.SourceRegistryProvenance)
}

// TestConvertGenericServersToTyped_NoOAuth verifies servers without OAuth have nil OAuth field
func TestConvertGenericServersToTyped_NoOAuth(t *testing.T) {
genericServers := []map[string]interface{}{
Expand Down
9 changes: 9 additions & 0 deletions internal/contracts/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ type Server struct {
// these fields.
Diagnostic *Diagnostic `json:"diagnostic,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
// MCP-901 — registry provenance of an upstream that was added from a
// registry. SourceRegistryID names the source registry (empty for
// manually-configured servers); SourceRegistryProvenance is the trust tag
// recorded at add time ("official/trusted" or "custom/unverified"). Both
// are projected from config.ServerConfig so the approval/quarantine view
// can render an "added from <registry> · unverified" origin badge. Optional
// and omitted when empty — clients that pre-date this treat them as absent.
SourceRegistryID string `json:"source_registry_id,omitempty"`
SourceRegistryProvenance string `json:"source_registry_provenance,omitempty"`
}

// Diagnostic is the REST-API representation of a classified server failure.
Expand Down
11 changes: 11 additions & 0 deletions internal/management/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,17 @@ func (s *service) ListServers(ctx context.Context) ([]*contracts.Server, *contra
srv.Diagnostic = d
}

// MCP-901 — project registry provenance so the approval/quarantine
// view can show a server's origin. The SSE servers.changed embed
// shares this projection (buildServersChangedPayload → ListServers),
// so it stays in parity automatically.
if regID, ok := srvRaw["source_registry_id"].(string); ok && regID != "" {
srv.SourceRegistryID = regID
}
if prov, ok := srvRaw["source_registry_provenance"].(string); ok && prov != "" {
srv.SourceRegistryProvenance = prov
}

servers = append(servers, srv)

// Update stats
Expand Down
38 changes: 38 additions & 0 deletions internal/management/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,44 @@ func TestListServers(t *testing.T) {
assert.Equal(t, 1, stats.QuarantinedServers)
})

// MCP-901: source registry provenance is projected onto contracts.Server so
// the approval/quarantine view (and the SSE servers.changed embed, which
// shares this projection) can show a server's origin.
t.Run("source registry provenance projected", func(t *testing.T) {
runtime := newMockRuntime()
runtime.servers = []map[string]interface{}{
{
"id": "everything",
"name": "everything",
"enabled": true,
"source_registry_id": "modelcontextprotocol",
"source_registry_provenance": "custom/unverified",
},
{
"id": "manual",
"name": "manual",
"enabled": true,
},
}

svc := NewService(runtime, cfg, "", emitter, nil, logger)
servers, _, err := svc.ListServers(context.Background())
require.NoError(t, err)
require.Len(t, servers, 2)

byName := map[string]*contracts.Server{}
for _, s := range servers {
byName[s.Name] = s
}
require.Contains(t, byName, "everything")
assert.Equal(t, "modelcontextprotocol", byName["everything"].SourceRegistryID)
assert.Equal(t, "custom/unverified", byName["everything"].SourceRegistryProvenance)

require.Contains(t, byName, "manual")
assert.Empty(t, byName["manual"].SourceRegistryID)
assert.Empty(t, byName["manual"].SourceRegistryProvenance)
})

// T094: Test that TotalTools only counts enabled servers' tools (Issue #285 fix)
t.Run("TotalTools excludes disabled servers", func(t *testing.T) {
runtime := newMockRuntime()
Expand Down
20 changes: 20 additions & 0 deletions internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,18 @@ func (r *Runtime) GetAllServers() ([]map[string]interface{}, error) {
serverMap["reconnect_on_use"] = true
}

// MCP-901: carry registry provenance through to the REST/SSE projection
// so the approval/quarantine view can show a server's origin. Empty for
// manually-configured servers.
if serverStatus.Config != nil {
if serverStatus.Config.SourceRegistryID != "" {
serverMap["source_registry_id"] = serverStatus.Config.SourceRegistryID
}
if serverStatus.Config.SourceRegistryProvenance != "" {
serverMap["source_registry_provenance"] = serverStatus.Config.SourceRegistryProvenance
}
}

// Spec 044: include structured diagnostic error when available.
if serverStatus.Diagnostic != nil {
d := serverStatus.Diagnostic
Expand Down Expand Up @@ -2021,6 +2033,14 @@ func (r *Runtime) getAllServersLegacy() ([]map[string]interface{}, error) {
"status": "unknown",
}

// MCP-901: registry provenance in parity with the StateView path.
if srv.SourceRegistryID != "" {
serverInfo["source_registry_id"] = srv.SourceRegistryID
}
if srv.SourceRegistryProvenance != "" {
serverInfo["source_registry_provenance"] = srv.SourceRegistryProvenance
}

// Try to get connection status
if r.upstreamManager != nil {
if client, exists := r.upstreamManager.GetClient(srv.Name); exists && client != nil {
Expand Down
2 changes: 1 addition & 1 deletion oas/docs.go

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions oas/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,18 @@ components:
$ref: '#/components/schemas/contracts.SecurityScanSummary'
should_retry:
type: boolean
source_registry_id:
description: |-
MCP-901 — registry provenance of an upstream that was added from a
registry. SourceRegistryID names the source registry (empty for
manually-configured servers); SourceRegistryProvenance is the trust tag
recorded at add time ("official/trusted" or "custom/unverified"). Both
are projected from config.ServerConfig so the approval/quarantine view
can render an "added from <registry> · unverified" origin badge. Optional
and omitted when empty — clients that pre-date this treat them as absent.
type: string
source_registry_provenance:
type: string
status:
type: string
token_expires_at:
Expand Down
Loading