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
90 changes: 90 additions & 0 deletions .github/workflows/mcpx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,96 @@ jobs:
working-directory: pkgs/mcpx
run: bun test plugin/opencode/mcpx/

lint:
name: "basic: lint"
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

# Approach C (#1996): the linters are the dev shell's, so their
# versions are pinned by flake.lock and this workflow passes no
# per-action version input of its own. The shell is the flake's
# minimal `mcpx-lint` shell, not the default: realising the full
# monorepo dev shell on a cold runner exceeded this job's timeout
# and the leg was cancelled before any linter ran (PR #2718).
- uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23
with:
extra-conf: |
accept-flake-config = true
# The default dev shell's closure needs the cgroups
# feature at build time. That gate is enforced by the nix
# daemon, whose config is written here — a client-side
# --extra-experimental-features flag alone does not lift
# it (verified on this PR: the flag was accepted and the
# build still died). Drew's NixOS machines never see this
# because the flake's nixConfig is fed into their system
# nix.settings; a stock runner's is not (#1996).
experimental-features = nix-command flakes cgroups

# shellcheck reads the repository's .shellcheckrc, which enables every
# optional check; --severity=warning keeps this leg to findings that
# are wrong rather than merely unidiomatic (the style class is
# treefmt's shfmt job). actionlint checks these workflow files.
- name: go vet, gofmt, shellcheck, actionlint
run: |
# The cgroups feature is enabled in the installer step's
# extra-conf above (the daemon enforces it); the flag here
# covers the client side of `nix develop` itself. Without
# both, this step dies with "experimental Nix feature
# 'cgroups' is disabled" before any linter runs (#1996).
nix develop .#mcpx-lint --extra-experimental-features cgroups --command bash -euo pipefail -c '
cd pkgs/mcpx
go vet ./...
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "gofmt would rewrite:"
echo "$unformatted"
exit 1
fi
cd ../..
shellcheck -x -P SCRIPTDIR --severity=warning pkgs/mcpx/scripts/*.sh
# actionlint is scoped to the mcpx workflows: #1996 assigns it
# the mcpx workflow set, not the whole repo (repo-wide findings
# are tracked separately). -S warning matches the shellcheck
# severity above, so info/style notes inside run: blocks do
# not fail this leg.
actionlint -shellcheck "shellcheck -S warning" .github/workflows/mcpx*.yml
'

# golangci-lint runs, but is NOT blocking yet — explicitly, per
# #1996's acceptance fallback. Its first full run (PR #2718, run
# 41131293079) reported 119 pre-existing findings across the
# module (errcheck 50, staticcheck 50, unused 17, ineffassign 2),
# far beyond this slice's baseline. They are filed as #2836;
# when #2836 lands, this step loses `continue-on-error` and the
# leg becomes fully blocking. Until then the findings print on
# every run, visible but not gating.
- name: golangci-lint (non-blocking until #2836)
continue-on-error: true
run: |
nix develop .#mcpx-lint --extra-experimental-features cgroups --command bash -euo pipefail -c '
cd pkgs/mcpx
golangci-lint run
'

plugin:
name: "basic: plugin tests"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7

# Every test file the plugin has, not the single daemon.test.ts #1996
# was written against. Ops tests skip unless OPS_LIVE_SOCKET is set:
# they need a running daemon, and this leg deliberately does not
# provide one (the Go test job is where the daemon itself is tested).
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

- name: bun test
working-directory: pkgs/mcpx
run: bun test plugin/opencode/mcpx/

vendorhash-repair:
name: vendorHash repair (bot dependency bumps only)
# Only on bot branches. A human changing dependencies should see the
Expand Down
21 changes: 21 additions & 0 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,27 @@ let
inputs.clan-core.packages.${system}.clan-cli
];
};

# Minimal shell for the mcpx CI lint leg (#1996). The
# default shell above is the whole monorepo environment
# (Rust, Android SDK, clan, Haskell, ...); realising it on
# a cold GitHub runner took longer than the lint job's
# whole 20-minute timeout and the job was cancelled
# before any linter ran (PR #2718, run 41122626395).
# This shell carries only the lint tools, from the same
# pinned nixpkgs-unstable, so the leg's tool versions are
# still flake-pinned (Approach C) but its closure is just
# the tools themselves.
mcpx-lint = pkgs.mkShell {
packages = with pkgs; [
# go_1_27 is the repo's Go toolchain (nix-common.nix
# fmtBins); plain `go` may lag the go.mod directive.
go_1_27
golangci-lint
shellcheck
actionlint
];
};
}
);
};
Expand Down
5 changes: 5 additions & 0 deletions nix-common.nix
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ let
# --- Go (mcpx) ---
# The go_1_27 toolchain is in fmtBins (it also provides gofmt); mcpx is the
# repository's Go program, built with the same toolchain (buildGo127Module).
# golangci-lint and actionlint are the mcpx CI lint leg's tools (#1996):
# `nix develop` is where their versions are pinned, so the workflow passes
# no per-action version input of its own.
mcpx
golangci-lint
actionlint

# --- Git hooks (lefthook.yml: treefmt pre-commit, conform commit-msg) ---
lefthook
Expand Down
16 changes: 16 additions & 0 deletions pkgs/mcpx/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# golangci-lint configuration for mcpx (#1996).
#
# Deliberately not `default: all`: gosec and the other opinionated sets are
# not blocking here, and turning them on without triaging their findings
# would make the leg fail on day one for reasons nobody chose. The standard
# set (errcheck, govet, ineffassign, staticcheck, unused) is blocking, and
# staticcheck runs every check it has, including the style and quickfix
# classes it leaves off by default.
version: "2"

linters:
default: standard
settings:
staticcheck:
checks:
- all
8 changes: 4 additions & 4 deletions pkgs/mcpx/internal/catalog/embeddings.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func (s *SubwordEmbedder) Backend() string {

// RemoteEmbedder queries an OpenAI/Ollama/Neon-compatible POST /v1/embeddings endpoint.
type RemoteEmbedder struct {
client *http.Client
url string
apiKey string
model string
client *http.Client
url string
apiKey string
model string
}

// NewRemoteEmbedder creates a remote embedding client.
Expand Down
1 change: 0 additions & 1 deletion pkgs/mcpx/internal/codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,3 @@ func TestOutputSchemaTypeGeneration(t *testing.T) {
t.Fatalf("module missing typed return signature or Attached export:\n%s", mod)
}
}

1 change: 0 additions & 1 deletion pkgs/mcpx/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,6 @@ func TestPreconditionsGating(t *testing.T) {
}
}


// #1923: server tags and per-tool tags survive resolution — the server's
// list verbatim, and toolTags keyed by tool name.
func TestResolveCarriesTags(t *testing.T) {
Expand Down
10 changes: 5 additions & 5 deletions pkgs/mcpx/internal/daemon/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,11 @@ func (r *Registry) Warm(ctx context.Context, force bool) map[string]error {

// NamespaceInfo is one row of `mcpx ls`.
type NamespaceInfo struct {
Namespace string `json:"namespace"`
Server string `json:"server"`
Tools int `json:"tools"`
Resources int `json:"resources"`
Description string `json:"description,omitempty"`
Namespace string `json:"namespace"`
Server string `json:"server"`
Tools int `json:"tools"`
Resources int `json:"resources"`
Description string `json:"description,omitempty"`
// Tags are the server's search vocabulary (#1923), surfaced so
// `mcpx ls` readers can see what a namespace answers to.
Tags []string `json:"tags,omitempty"`
Expand Down
6 changes: 3 additions & 3 deletions pkgs/mcpx/internal/daemon/routes_consumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ func TestHandleDiagnose_RepairSafety(t *testing.T) {
reg := &Registry{
testTools: []ToolInfo{
{
Namespace: "calc",
Tool: "add",
Function: "calc.add",
Namespace: "calc",
Tool: "add",
Function: "calc.add",
InputSchema: []byte(`{"type":"object","required":["a","b"]}`),
},
},
Expand Down
28 changes: 14 additions & 14 deletions pkgs/mcpx/internal/logstore/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ const (

// InteractionRecord represents an input/output turn or tool execution with its trace ID and feedback.
type InteractionRecord struct {
TraceID string `json:"traceId"`
Timestamp time.Time `json:"timestamp"`
SessionID string `json:"sessionId,omitempty"`
Source string `json:"source"` // "proxy", "call", "task", etc.
Input string `json:"input"`
Output string `json:"output,omitempty"`
ToolsUsed []string `json:"toolsUsed,omitempty"`
Score *float64 `json:"score,omitempty"` // primary score (e.g. 1.0 = good, 0.0 = bad)
Target string `json:"target,omitempty"` // "retrieval" vs "execution"
RetrievalScore *float64 `json:"retrievalScore,omitempty"` // score for search / tool match relevance
ExecutionScore *float64 `json:"executionScore,omitempty"` // score for tool runtime behavior
ScoresJSON string `json:"scoresJson,omitempty"` // multi-dimension representation (e.g. {"relevance": 1.0, "latency": 0.8})
FeedbackNotes string `json:"feedbackNotes,omitempty"`
FeedbackAt *time.Time `json:"feedbackAt,omitempty"`
TraceID string `json:"traceId"`
Timestamp time.Time `json:"timestamp"`
SessionID string `json:"sessionId,omitempty"`
Source string `json:"source"` // "proxy", "call", "task", etc.
Input string `json:"input"`
Output string `json:"output,omitempty"`
ToolsUsed []string `json:"toolsUsed,omitempty"`
Score *float64 `json:"score,omitempty"` // primary score (e.g. 1.0 = good, 0.0 = bad)
Target string `json:"target,omitempty"` // "retrieval" vs "execution"
RetrievalScore *float64 `json:"retrievalScore,omitempty"` // score for search / tool match relevance
ExecutionScore *float64 `json:"executionScore,omitempty"` // score for tool runtime behavior
ScoresJSON string `json:"scoresJson,omitempty"` // multi-dimension representation (e.g. {"relevance": 1.0, "latency": 0.8})
FeedbackNotes string `json:"feedbackNotes,omitempty"`
FeedbackAt *time.Time `json:"feedbackAt,omitempty"`
}

// InteractionFilter defines parameters for filtering interaction history.
Expand Down
4 changes: 2 additions & 2 deletions pkgs/mcpx/internal/mcpserver/reactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (s *Server) reactiveSurface(ctx context.Context) ([]Tool, error) {
var out []Tool
// 1. Core meta tools
out = append(out, Tool{
Name: "request_tools",
Name: "request_tools",
Description: "Search for and activate additional tools into the active tool set matching a natural language query.",
InputSchema: json.RawMessage(`{
"type": "object",
Expand All @@ -28,7 +28,7 @@ func (s *Server) reactiveSurface(ctx context.Context) ([]Tool, error) {
}`),
})
out = append(out, Tool{
Name: "batch_call",
Name: "batch_call",
Description: "Execute multiple tool calls in a single turn.",
InputSchema: json.RawMessage(`{
"type": "object",
Expand Down
8 changes: 4 additions & 4 deletions pkgs/mcpx/internal/mcpserver/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ func frontmatter(md []byte) (map[string]any, error) {

// UpstreamSkill represents a skill contributed by an upstream server.
type UpstreamSkill struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Frontmatter map[string]any `json:"frontmatter,omitempty"`
Namespace string `json:"namespace"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Frontmatter map[string]any `json:"frontmatter,omitempty"`
Files []UpstreamSkillFile `json:"files,omitempty"`
}

Expand Down
12 changes: 6 additions & 6 deletions pkgs/mcpx/internal/scripting/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ type Provider interface {

// ProviderConfig configures model resolution.
type ProviderConfig struct {
Type string `json:"type"` // "openai", "ollama", "qwen", "claude", "codex", "opencode", "agy", "command"
BaseURL string `json:"baseUrl"` // e.g. "http://localhost:11434/v1", "https://api.openai.com/v1"
APIKey string `json:"apiKey"` // API key if required
Model string `json:"model"` // Model name, e.g. "qwen2.5-coder", "gpt-4o"
Command string `json:"command"` // CLI command if Type == "command"
Timeout time.Duration
Type string `json:"type"` // "openai", "ollama", "qwen", "claude", "codex", "opencode", "agy", "command"
BaseURL string `json:"baseUrl"` // e.g. "http://localhost:11434/v1", "https://api.openai.com/v1"
APIKey string `json:"apiKey"` // API key if required
Model string `json:"model"` // Model name, e.g. "qwen2.5-coder", "gpt-4o"
Command string `json:"command"` // CLI command if Type == "command"
Timeout time.Duration
}

// OpenAICompatibleProvider handles any OpenAI-compatible completions API
Expand Down
1 change: 0 additions & 1 deletion pkgs/mcpx/internal/scripting/scripting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,3 @@ func TestResolveProviderNamedDrivers(t *testing.T) {
t.Fatalf("expected auto provider to be opencode or claude, got %s", pAuto.Name())
}
}

1 change: 1 addition & 0 deletions pkgs/mcpx/internal/testsupport/quarantine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (f *fakeTB) Helper() {}
func (f *fakeTB) Skipf(format string, args ...any) {
f.skipped = true
}

// errFakeFatal is the panic a fake Fatalf raises to model what the real
// testing.T.Fatalf does: it does not return. Without this the flow under
// test continues past the fatal call and the double records a skip after
Expand Down
5 changes: 2 additions & 3 deletions pkgs/mcpx/scripts/stress.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ set -uo pipefail
MCPX=${MCPX:?set MCPX to the mcpx binary}
ROUNDS=${ROUNDS:-5}
CONC=${CONC:-3}
here=$(cd "$(dirname "$0")" && pwd)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

Expand Down Expand Up @@ -83,8 +82,8 @@ maxsum=$("$MCPX" --json status | python3 -c '
import json,sys
d=json.load(sys.stdin)
print(sum(s["max"] for s in d["servers"]))')
[ "$live" -le "$maxsum" ]
check $? "live instances ($live) within configured maxima ($maxsum)"
if [ "$live" -le "$maxsum" ]; then within=0; else within=1; fi
check "$within" "live instances ($live) within configured maxima ($maxsum)"

note "== orphan check =="
"$MCPX" restart chrome-devtools >/dev/null
Expand Down
Loading