Skip to content

Commit edf3929

Browse files
domdomeggdmartinolclaude
authored
fix: Named arguments should identify the argument name (#321)
Originally by @dmartinol. This branch is an attempt by @domdomegg to fix the merge conflicts... --- ## Motivation and Context Closes #145 Add comprehensive validation for named arguments in MCP server packages to ensure argument names, values, and defaults follow proper format conventions. This prevents common issues where argument names contain embedded values or descriptions, and where values/defaults incorrectly start with the argument name itself. Key changes: - Add ArgumentValidator with validation rules for named argument format - Enhance PackageValidator to validate runtime and package arguments - Update import process to log warnings for invalid servers instead of failing - Fix 396 servers in seed.json with malformed named arguments - Extended `validate-examples` tool to apply the object validator (it was completely missed before) - Add comprehensive test coverage for all validation scenarios Examples of fixes applied: - "--directory <path>" → "--directory" with value "<path>" - "--with-editable $REPO" → "--with-editable" with value "$REPO" - "recipient@example.com" → "--recipient" (proper argument naming) All servers now pass validation during both API publishing and data import. 🤖 Generated with [Claude Code](https://claude.ai/code) ## How Has This Been Tested? With ad-hoc unit tests. Manual integration tests of built artifact and the seed data. ## Breaking Changes New registrations need to follow the argument name conventions, otherwise the import would fail for the inconsistent servers. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [ ] I have added or updated documentation as needed --------- Signed-off-by: Daniele Martinoli <dmartino@redhat.com> Co-authored-by: Daniele Martinoli <dmartino@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8afe162 commit edf3929

File tree

7 files changed

+1099
-748
lines changed

7 files changed

+1099
-748
lines changed

data/seed.json

Lines changed: 696 additions & 696 deletions
Large diffs are not rendered by default.

internal/database/import.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"log"
89
"net/http"
910
"os"
1011
"strings"
1112

13+
"github.com/modelcontextprotocol/registry/internal/validators"
1214
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
1315
)
1416

@@ -48,14 +50,37 @@ func ReadSeedFile(ctx context.Context, path string) ([]*apiv0.ServerRecord, erro
4850
return []*apiv0.ServerRecord{}, nil
4951
}
5052

51-
// Convert ServerResponse to ServerRecord
52-
var records []*apiv0.ServerRecord
53+
// Validate servers and collect warnings instead of failing the whole batch
54+
var validRecords []*apiv0.ServerRecord
55+
var invalidServers []string
56+
var validationFailures []string
57+
5358
for _, response := range serverResponses {
59+
if err := validators.ValidateServerJSON(&response.Server); err != nil {
60+
// Log warning and track invalid server instead of failing
61+
invalidServers = append(invalidServers, response.Server.Name)
62+
validationFailures = append(validationFailures, fmt.Sprintf("Server '%s': %v", response.Server.Name, err))
63+
log.Printf("Warning: Skipping invalid server '%s': %v", response.Server.Name, err)
64+
continue
65+
}
66+
67+
// Convert valid ServerResponse to ServerRecord
5468
record := convertServerResponseToRecord(response)
55-
records = append(records, record)
69+
validRecords = append(validRecords, record)
70+
}
71+
72+
// Print summary of validation results
73+
if len(invalidServers) > 0 {
74+
log.Printf("Import summary: %d valid servers imported, %d invalid servers skipped", len(validRecords), len(invalidServers))
75+
log.Printf("Invalid servers: %v", invalidServers)
76+
for _, failure := range validationFailures {
77+
log.Printf(" - %s", failure)
78+
}
79+
} else {
80+
log.Printf("Import summary: All %d servers imported successfully", len(validRecords))
5681
}
5782

58-
return records, nil
83+
return validRecords, nil
5984
}
6085

6186
func fetchFromHTTP(ctx context.Context, url string) ([]byte, error) {

internal/database/import_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestReadSeedFile_LocalFile(t *testing.T) {
2121
seedData := []apiv0.ServerRecord{
2222
{
2323
Server: model.ServerJSON{
24-
Name: "test-server-1",
24+
Name: "com.example/test-server-1",
2525
Description: "Test server 1",
2626
Repository: model.Repository{
2727
URL: "https://github.com/test/repo1",
@@ -31,6 +31,11 @@ func TestReadSeedFile_LocalFile(t *testing.T) {
3131
VersionDetail: model.VersionDetail{
3232
Version: "1.0.0",
3333
},
34+
Remotes: []model.Remote{
35+
{
36+
URL: "https://example.com/remote",
37+
},
38+
},
3439
},
3540
XIOModelContextProtocolRegistry: apiv0.RegistryExtensions{
3641
ID: "test-id-1",
@@ -61,16 +66,21 @@ func TestReadSeedFile_LocalFile(t *testing.T) {
6166
result, err := database.ReadSeedFile(context.Background(), tempFile)
6267
assert.NoError(t, err)
6368
assert.Len(t, result, 1)
64-
assert.Equal(t, "test-server-1", result[0].Server.Name)
69+
assert.Equal(t, "com.example/test-server-1", result[0].Server.Name)
6570
}
6671

6772
func TestReadSeedFile_DirectHTTPURL(t *testing.T) {
6873
// Create a test HTTP server that serves seed JSON directly in extension wrapper format
6974
seedData := []apiv0.ServerRecord{
7075
{
7176
Server: model.ServerJSON{
72-
Name: "test-server-1",
77+
Name: "com.example/test-server-1",
7378
Description: "Test server 1",
79+
Remotes: []model.Remote{
80+
{
81+
URL: "https://example.com/remote",
82+
},
83+
},
7484
},
7585
XIOModelContextProtocolRegistry: apiv0.RegistryExtensions{
7686
ID: "test-id-1",
@@ -93,7 +103,7 @@ func TestReadSeedFile_DirectHTTPURL(t *testing.T) {
93103
result, err := database.ReadSeedFile(context.Background(), server.URL+"/seed.json")
94104
assert.NoError(t, err)
95105
assert.Len(t, result, 1)
96-
assert.Equal(t, "test-server-1", result[0].Server.Name)
106+
assert.Equal(t, "com.example/test-server-1", result[0].Server.Name)
97107
}
98108

99109
func TestReadSeedFile_RegistryURL(t *testing.T) {

internal/validators/constants.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ var (
1212

1313
// Remote validation errors
1414
ErrInvalidRemoteURL = errors.New("invalid remote URL")
15+
16+
// Argument validation errors
17+
ErrNamedArgumentNameRequired = errors.New("named argument name is required")
18+
ErrInvalidNamedArgumentName = errors.New("invalid named argument name format")
19+
ErrArgumentValueStartsWithName = errors.New("argument value cannot start with the argument name")
20+
ErrArgumentDefaultStartsWithName = errors.New("argument default cannot start with the argument name")
1521
)
1622

1723
// RepositorySource represents valid repository sources

internal/validators/validators.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,66 @@ func validatePackageField(obj *model.Package) error {
7171
return ErrPackageNameHasSpaces
7272
}
7373

74+
// Validate runtime arguments
75+
for _, arg := range obj.RuntimeArguments {
76+
if err := validateArgument(&arg); err != nil {
77+
return fmt.Errorf("invalid runtime argument: %w", err)
78+
}
79+
}
80+
81+
// Validate package arguments
82+
for _, arg := range obj.PackageArguments {
83+
if err := validateArgument(&arg); err != nil {
84+
return fmt.Errorf("invalid package argument: %w", err)
85+
}
86+
}
87+
88+
return nil
89+
}
90+
91+
// validateArgument validates argument details
92+
func validateArgument(obj *model.Argument) error {
93+
if obj.Type == model.ArgumentTypeNamed {
94+
// Validate named argument name format
95+
if err := validateNamedArgumentName(obj.Name); err != nil {
96+
return err
97+
}
98+
99+
// Validate value and default don't start with the name
100+
if err := validateArgumentValueFields(obj.Name, obj.Value, obj.Default); err != nil {
101+
return err
102+
}
103+
}
104+
return nil
105+
}
106+
107+
func validateNamedArgumentName(name string) error {
108+
// Check if name is required for named arguments
109+
if name == "" {
110+
return ErrNamedArgumentNameRequired
111+
}
112+
113+
// Check for invalid characters that suggest embedded values or descriptions
114+
// Valid: "--directory", "--port", "-v", "config", "verbose"
115+
// Invalid: "--directory <absolute_path_to_adfin_mcp_folder>", "--port 8080"
116+
if strings.Contains(name, "<") || strings.Contains(name, ">") ||
117+
strings.Contains(name, " ") || strings.Contains(name, "$") {
118+
return fmt.Errorf("%w: %s", ErrInvalidNamedArgumentName, name)
119+
}
120+
121+
return nil
122+
}
123+
124+
func validateArgumentValueFields(name, value, defaultValue string) error {
125+
// Check if value starts with the argument name (using startsWith, not contains)
126+
if value != "" && strings.HasPrefix(value, name) {
127+
return fmt.Errorf("%w: value starts with argument name '%s': %s", ErrArgumentValueStartsWithName, name, value)
128+
}
129+
130+
if defaultValue != "" && strings.HasPrefix(defaultValue, name) {
131+
return fmt.Errorf("%w: default starts with argument name '%s': %s", ErrArgumentDefaultStartsWithName, name, defaultValue)
132+
}
133+
74134
return nil
75135
}
76136

0 commit comments

Comments
 (0)