a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
193 lines
7.0 KiB
Go
193 lines
7.0 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// hasMCPEdge reports whether an edge of the given kind from→to exists.
|
|
// (nodeByID is shared with other tests in this package.)
|
|
func hasMCPEdge(edges []*graph.Edge, from, to string, kind graph.EdgeKind) bool {
|
|
for _, e := range edges {
|
|
if e.From == from && e.To == to && e.Kind == kind {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestMCPConfigExtractor_NpmAndPypiServers(t *testing.T) {
|
|
src := []byte(`{
|
|
"mcpServers": {
|
|
"github": {
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
"env": {"GITHUB_TOKEN": "x"}
|
|
},
|
|
"fetch": {
|
|
"command": "uvx",
|
|
"args": ["mcp-server-fetch"]
|
|
}
|
|
}
|
|
}`)
|
|
const fp = ".mcp.json"
|
|
e := NewMCPConfigExtractor()
|
|
result, err := e.Extract(fp, src)
|
|
require.NoError(t, err)
|
|
|
|
// File node.
|
|
files := nodesOfKind(result.Nodes, graph.KindFile)
|
|
require.Len(t, files, 1)
|
|
assert.Equal(t, fp, files[0].ID)
|
|
|
|
// Both servers as KindResource.
|
|
githubServer := nodeByID(result.Nodes, "mcp::server::"+fp+"::github")
|
|
require.NotNil(t, githubServer, "github server node should exist")
|
|
assert.Equal(t, graph.KindResource, githubServer.Kind)
|
|
assert.Equal(t, "github", githubServer.Name)
|
|
assert.Equal(t, "mcp_server", githubServer.Meta["resource_type"])
|
|
assert.Equal(t, "npx", githubServer.Meta["command"])
|
|
assert.Equal(t, "stdio", githubServer.Meta["transport"])
|
|
assert.Equal(t, ".mcp.json", githubServer.Meta["source"])
|
|
assert.True(t, hasMCPEdge(result.Edges, fp, githubServer.ID, graph.EdgeDefines))
|
|
|
|
fetchServer := nodeByID(result.Nodes, "mcp::server::"+fp+"::fetch")
|
|
require.NotNil(t, fetchServer, "fetch server node should exist")
|
|
assert.Equal(t, graph.KindResource, fetchServer.Kind)
|
|
|
|
// npm module for the github server.
|
|
const npmID = "module::npm:@modelcontextprotocol/server-github"
|
|
npmMod := nodeByID(result.Nodes, npmID)
|
|
require.NotNil(t, npmMod, "npm module node should exist with exact ID")
|
|
assert.Equal(t, graph.KindModule, npmMod.Kind)
|
|
assert.Equal(t, "npm", npmMod.Meta["ecosystem"])
|
|
assert.True(t, hasMCPEdge(result.Edges, githubServer.ID, npmID, graph.EdgeDependsOnModule),
|
|
"github server should depend on the npm module")
|
|
|
|
// pypi module for the fetch server.
|
|
const pypiID = "module::pypi:mcp-server-fetch"
|
|
pypiMod := nodeByID(result.Nodes, pypiID)
|
|
require.NotNil(t, pypiMod, "pypi module node should exist with exact ID")
|
|
assert.Equal(t, graph.KindModule, pypiMod.Kind)
|
|
assert.Equal(t, "pypi", pypiMod.Meta["ecosystem"])
|
|
assert.True(t, hasMCPEdge(result.Edges, fetchServer.ID, pypiID, graph.EdgeDependsOnModule),
|
|
"fetch server should depend on the pypi module")
|
|
|
|
// Env config key — ID must be exactly cfg::env::GITHUB_TOKEN so it
|
|
// shares identity with os.Getenv("GITHUB_TOKEN") code nodes.
|
|
const envID = "cfg::env::GITHUB_TOKEN"
|
|
envKey := nodeByID(result.Nodes, envID)
|
|
require.NotNil(t, envKey, "env config key node should exist with exact ID cfg::env::GITHUB_TOKEN")
|
|
assert.Equal(t, graph.KindConfigKey, envKey.Kind)
|
|
assert.Equal(t, "GITHUB_TOKEN", envKey.Name)
|
|
assert.Equal(t, "env", envKey.Meta["source"])
|
|
assert.True(t, hasMCPEdge(result.Edges, githubServer.ID, envID, graph.EdgeUsesEnv),
|
|
"github server should use_env GITHUB_TOKEN")
|
|
}
|
|
|
|
func TestMCPConfigExtractor_ClaudeDesktopShape(t *testing.T) {
|
|
// claude_desktop_config.json uses the same mcpServers shape, with a
|
|
// scoped+versioned npm spec and a ${VAR} arg interpolation.
|
|
src := []byte(`{
|
|
"mcpServers": {
|
|
"filesystem": {
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-filesystem@1.2.3", "${HOME_DIR}"],
|
|
"env": {"FS_ROOT": "/data"}
|
|
}
|
|
}
|
|
}`)
|
|
const fp = "claude_desktop_config.json"
|
|
e := NewMCPConfigExtractor()
|
|
result, err := e.Extract(fp, src)
|
|
require.NoError(t, err)
|
|
|
|
server := nodeByID(result.Nodes, "mcp::server::"+fp+"::filesystem")
|
|
require.NotNil(t, server)
|
|
assert.Equal(t, "claude_desktop_config.json", server.Meta["source"])
|
|
|
|
// Scoped + versioned npm spec splits into name + version.
|
|
const npmID = "module::npm:@modelcontextprotocol/server-filesystem@1.2.3"
|
|
npmMod := nodeByID(result.Nodes, npmID)
|
|
require.NotNil(t, npmMod, "versioned scoped npm module node should exist")
|
|
assert.Equal(t, "@modelcontextprotocol/server-filesystem", npmMod.Name)
|
|
assert.Equal(t, "1.2.3", npmMod.Meta["version"])
|
|
assert.True(t, hasMCPEdge(result.Edges, server.ID, npmID, graph.EdgeDependsOnModule))
|
|
|
|
// Declared env var.
|
|
assert.True(t, hasMCPEdge(result.Edges, server.ID, "cfg::env::FS_ROOT", graph.EdgeUsesEnv))
|
|
// ${HOME_DIR} arg interpolation.
|
|
homeKey := nodeByID(result.Nodes, "cfg::env::HOME_DIR")
|
|
require.NotNil(t, homeKey, "interpolated ${HOME_DIR} should produce a config key")
|
|
assert.True(t, hasMCPEdge(result.Edges, server.ID, "cfg::env::HOME_DIR", graph.EdgeUsesEnv))
|
|
}
|
|
|
|
func TestMCPConfigExtractor_DockerServer(t *testing.T) {
|
|
src := []byte(`{
|
|
"mcpServers": {
|
|
"gitlab": {
|
|
"command": "docker",
|
|
"args": ["run", "-i", "--rm", "-e", "GITLAB_TOKEN", "mcp/gitlab:1.5"],
|
|
"env": {"GITLAB_TOKEN": "secret"}
|
|
}
|
|
}
|
|
}`)
|
|
const fp = ".mcp.json"
|
|
e := NewMCPConfigExtractor()
|
|
result, err := e.Extract(fp, src)
|
|
require.NoError(t, err)
|
|
|
|
server := nodeByID(result.Nodes, "mcp::server::"+fp+"::gitlab")
|
|
require.NotNil(t, server)
|
|
|
|
// Image node — the flag-value (-e GITLAB_TOKEN) must not be mistaken
|
|
// for the image; the image is mcp/gitlab:1.5.
|
|
const imgID = "image::mcp/gitlab:1.5"
|
|
img := nodeByID(result.Nodes, imgID)
|
|
require.NotNil(t, img, "docker image node should exist with exact ID image::mcp/gitlab:1.5")
|
|
assert.Equal(t, graph.KindImage, img.Kind)
|
|
assert.Equal(t, "1.5", img.Meta["tag"])
|
|
assert.True(t, hasMCPEdge(result.Edges, server.ID, imgID, graph.EdgeDependsOn),
|
|
"docker server should depend_on the image")
|
|
|
|
// No module node should be emitted for a docker command.
|
|
assert.Empty(t, nodesOfKind(result.Nodes, graph.KindModule))
|
|
|
|
// Env var still wired.
|
|
assert.True(t, hasMCPEdge(result.Edges, server.ID, "cfg::env::GITLAB_TOKEN", graph.EdgeUsesEnv))
|
|
}
|
|
|
|
func TestMCPConfigExtractor_VSCodeServersKey(t *testing.T) {
|
|
// VS Code uses a top-level "servers" key and a remote http transport.
|
|
src := []byte(`{
|
|
"servers": {
|
|
"remote": {
|
|
"type": "http",
|
|
"url": "https://mcp.example.com/sse"
|
|
}
|
|
}
|
|
}`)
|
|
const fp = "mcp.json"
|
|
e := NewMCPConfigExtractor()
|
|
result, err := e.Extract(fp, src)
|
|
require.NoError(t, err)
|
|
|
|
server := nodeByID(result.Nodes, "mcp::server::"+fp+"::remote")
|
|
require.NotNil(t, server, "servers-key layout should be accepted")
|
|
assert.Equal(t, "http", server.Meta["transport"])
|
|
}
|
|
|
|
func TestMCPConfigExtractor_Extensions(t *testing.T) {
|
|
e := NewMCPConfigExtractor()
|
|
exts := e.Extensions()
|
|
assert.Contains(t, exts, ".mcp.json")
|
|
assert.Contains(t, exts, "mcp.json")
|
|
assert.Contains(t, exts, "claude_desktop_config.json")
|
|
// Critically: must NOT claim the bare .json extension, or it would
|
|
// steal package.json / tsconfig.json from the generic JSON extractor.
|
|
assert.NotContains(t, exts, ".json")
|
|
}
|