Skip to content

Commit 1010a33

Browse files
committed
Update for working PyPi, Nuget and Docker validation
🏠 Remote-Dev: homespace
1 parent 70a63e0 commit 1010a33

File tree

10 files changed

+232
-86
lines changed

10 files changed

+232
-86
lines changed

docs/publisher/guide.md

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,15 @@ The official MCP registry currently only supports the NPM public registry (`http
8888
<summary><strong>🐍 PyPI Packages</strong></summary>
8989

9090
### Requirements
91-
Add an `MCP` project URL to your `pyproject.toml`:
91+
Include your server name in your package README file using this format:
9292

93-
```toml
94-
[project.urls]
95-
Homepage = "https://github.com/you/your-project"
96-
MCP = "io.github.username/server-name"
97-
```
93+
**MCP name format**: `mcp-name: io.github.username/server-name`
94+
95+
Add it to your README.md file (which becomes the package description on PyPI).
9896

9997
### How It Works
10098
- Registry fetches `https://pypi.org/pypi/your-package/json`
101-
- Checks that `project_urls["MCP"]` matches your server name
102-
- Fails if MCP URL is missing or doesn't match
99+
- Passes if `mcp-name: server-name` is in the README content
103100

104101
### Example server.json
105102
```json
@@ -123,24 +120,15 @@ The official MCP registry currently only supports the official PyPI registry (`h
123120
<summary><strong>📋 NuGet Packages</strong></summary>
124121

125122
### Requirements
126-
Add an `<mcp-name>` element to your `.nuspec` file:
127-
128-
```xml
129-
<package>
130-
<metadata>
131-
<id>Your.NuGet.Package</id>
132-
<version>1.0.0</version>
133-
<mcp-name>io.github.username/server-name</mcp-name>
134-
<!-- other metadata -->
135-
</metadata>
136-
</package>
137-
```
123+
Include your server name in your package's README using this format:
124+
125+
**MCP name format**: `mcp-name: io.github.username/server-name`
126+
127+
Add a README file to your NuGet package that includes the server name.
138128

139129
### How It Works
140-
- Registry fetches `.nuspec` file directly from NuGet API
141-
- Uses URL pattern: `https://api.nuget.org/v3-flatcontainer/{id}/{version}/{id}.nuspec`
142-
- Checks that `<mcp-name>` element matches your server name
143-
- Fails if element is missing or doesn't match
130+
- Registry fetches README from `https://api.nuget.org/v3-flatcontainer/{id}/{version}/readme`
131+
- Passes if `mcp-name: server-name` is found in the README content
144132

145133
### Example server.json
146134
```json

internal/validators/registries/mcpb_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ func TestValidateMCPB(t *testing.T) {
1919
expectError bool
2020
errorMessage string
2121
}{
22+
{
23+
name: "valid MCPB package should pass",
24+
packageName: "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb",
25+
serverName: "io.github.domdomegg/airtable-mcp-server",
26+
expectError: false,
27+
},
2228
{
2329
name: "valid MCPB package should pass",
2430
packageName: "https://github.com/microsoft/playwright-mcp/releases/download/v0.0.36/playwright-mcp-extension-v0.0.36.zip",

internal/validators/registries/npm.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ func ValidateNPM(ctx context.Context, pkg model.Package, serverName string) erro
2323
}
2424

2525
client := &http.Client{Timeout: 10 * time.Second}
26-
27-
url := baseURL + "/" + pkg.Identifier
26+
27+
url := baseURL + "/" + pkg.Identifier + "/" + pkg.Version
2828
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2929
if err != nil {
3030
return fmt.Errorf("failed to create request: %w", err)
3131
}
32-
32+
3333
req.Header.Set("User-Agent", "MCP-Registry-Validator/1.0")
3434
req.Header.Set("Accept", "application/json")
3535

@@ -57,4 +57,4 @@ func ValidateNPM(ctx context.Context, pkg model.Package, serverName string) erro
5757
}
5858

5959
return nil
60-
}
60+
}

internal/validators/registries/npm_test.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,35 @@ func TestValidateNPM_RealPackages(t *testing.T) {
3737
errorMessage: "missing required 'mcpName' field",
3838
},
3939
{
40-
name: "real package with different mcpName should fail",
40+
name: "real package without mcpName should fail",
4141
packageName: "lodash", // Another popular package
4242
version: "4.17.21",
4343
serverName: "com.example/completely-different-name",
4444
expectError: true,
45-
errorMessage: "missing required 'mcpName' field", // Will fail because lodash doesn't have mcpName
45+
errorMessage: "missing required 'mcpName' field",
46+
},
47+
{
48+
name: "real package without mcpName should fail",
49+
packageName: "airtable-mcp-server",
50+
version: "1.5.0",
51+
serverName: "io.github.domdomegg/airtable-mcp-server",
52+
expectError: true,
53+
errorMessage: "missing required 'mcpName' field",
54+
},
55+
{
56+
name: "real package with incorrect mcpName should fail",
57+
packageName: "airtable-mcp-server",
58+
version: "1.7.2",
59+
serverName: "io.github.not-domdomegg/airtable-mcp-server",
60+
expectError: true,
61+
errorMessage: "Expected mcpName 'io.github.not-domdomegg/airtable-mcp-server', got 'io.github.domdomegg/airtable-mcp-server'",
62+
},
63+
{
64+
name: "real package with correct mcpName should pass",
65+
packageName: "airtable-mcp-server",
66+
version: "1.7.2",
67+
serverName: "io.github.domdomegg/airtable-mcp-server",
68+
expectError: false,
4669
},
4770
}
4871

@@ -64,4 +87,4 @@ func TestValidateNPM_RealPackages(t *testing.T) {
6487
}
6588
})
6689
}
67-
}
90+
}

internal/validators/registries/nuget.go

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,15 @@ package registries
22

33
import (
44
"context"
5-
"encoding/xml"
65
"fmt"
6+
"io"
77
"net/http"
88
"strings"
99
"time"
1010

1111
"github.com/modelcontextprotocol/registry/pkg/model"
1212
)
1313

14-
// NuSpecMetadata represents the metadata section of a .nuspec file
15-
type NuSpecMetadata struct {
16-
XMLName xml.Name `xml:"metadata"`
17-
MCPName string `xml:"mcp-name"`
18-
}
19-
20-
// NuSpecPackage represents the root element of a .nuspec file
21-
type NuSpecPackage struct {
22-
XMLName xml.Name `xml:"package"`
23-
Metadata NuSpecMetadata `xml:"metadata"`
24-
}
25-
2614
// ValidateNuGet validates that a NuGet package contains the correct MCP server name
2715
func ValidateNuGet(ctx context.Context, pkg model.Package, serverName string) error {
2816
baseURL := pkg.RegistryBaseURL
@@ -38,8 +26,9 @@ func ValidateNuGet(ctx context.Context, pkg model.Package, serverName string) er
3826
return fmt.Errorf("NuGet package validation requires a specific version, but none was provided")
3927
}
4028

41-
url := fmt.Sprintf("%s/v3-flatcontainer/%s/%s/%s.nuspec", baseURL, lowerID, lowerVersion, lowerID)
42-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
29+
// Try to get README from the package
30+
readmeURL := fmt.Sprintf("%s/v3-flatcontainer/%s/%s/readme", baseURL, lowerID, lowerVersion)
31+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, readmeURL, nil)
4332
if err != nil {
4433
return fmt.Errorf("failed to create request: %w", err)
4534
}
@@ -48,26 +37,25 @@ func ValidateNuGet(ctx context.Context, pkg model.Package, serverName string) er
4837

4938
resp, err := client.Do(req)
5039
if err != nil {
51-
return fmt.Errorf("failed to fetch .nuspec from NuGet: %w", err)
40+
return fmt.Errorf("failed to fetch README from NuGet: %w", err)
5241
}
5342
defer resp.Body.Close()
5443

55-
if resp.StatusCode != http.StatusOK {
56-
return fmt.Errorf("NuGet package '%s' version '%s' not found (status: %d)", pkg.Identifier, pkg.Version, resp.StatusCode)
57-
}
58-
59-
var nuspec NuSpecPackage
60-
if err := xml.NewDecoder(resp.Body).Decode(&nuspec); err != nil {
61-
return fmt.Errorf("failed to parse .nuspec metadata: %w", err)
62-
}
44+
if resp.StatusCode == http.StatusOK {
45+
// Check README content
46+
readmeBytes, err := io.ReadAll(resp.Body)
47+
if err != nil {
48+
return fmt.Errorf("failed to read README content: %w", err)
49+
}
6350

64-
if nuspec.Metadata.MCPName == "" {
65-
return fmt.Errorf("NuGet package '%s' is missing required '<mcp-name>' element. Add this to your .nuspec: <mcp-name>%s</mcp-name>", pkg.Identifier, serverName)
66-
}
51+
readmeContent := string(readmeBytes)
6752

68-
if nuspec.Metadata.MCPName != serverName {
69-
return fmt.Errorf("NuGet package ownership validation failed. Expected mcp-name '%s', got '%s'", serverName, nuspec.Metadata.MCPName)
53+
// Check for mcp-name: format (more specific)
54+
mcpNamePattern := "mcp-name: " + serverName
55+
if strings.Contains(readmeContent, mcpNamePattern) {
56+
return nil // Found as mcp-name: format
57+
}
7058
}
7159

72-
return nil
60+
return fmt.Errorf("NuGet package '%s' ownership validation failed. The server name '%s' must appear as 'mcp-name: %s' in the package README. Add it to your package README", pkg.Identifier, serverName, serverName)
7361
}

internal/validators/registries/nuget_test.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestValidateNuGet_RealPackages(t *testing.T) {
2626
version: "1.0.0",
2727
serverName: "com.example/test",
2828
expectError: true,
29-
errorMessage: "not found",
29+
errorMessage: "ownership validation failed",
3030
},
3131
{
3232
name: "real package without version should fail",
@@ -42,15 +42,30 @@ func TestValidateNuGet_RealPackages(t *testing.T) {
4242
version: "999.999.999", // Version that doesn't exist
4343
serverName: "com.example/test",
4444
expectError: true,
45-
errorMessage: "not found",
45+
errorMessage: "ownership validation failed",
4646
},
4747
{
48-
name: "real package without mcp-name should fail",
48+
name: "real package without server name in README should fail",
4949
packageName: "Newtonsoft.Json",
5050
version: "13.0.3", // Popular version
5151
serverName: "com.example/test",
5252
expectError: true,
53-
errorMessage: "missing required '<mcp-name>' element",
53+
errorMessage: "ownership validation failed",
54+
},
55+
{
56+
name: "real package without server name in README should fail",
57+
packageName: "TimeMcpServer",
58+
version: "1.0.0",
59+
serverName: "io.github.domdomegg/time-mcp-server",
60+
expectError: true,
61+
errorMessage: "ownership validation failed",
62+
},
63+
{
64+
name: "real package with server name in README should pass",
65+
packageName: "TimeMcpServer",
66+
version: "1.0.2",
67+
serverName: "io.github.domdomegg/time-mcp-server",
68+
expectError: false,
5469
},
5570
}
5671

@@ -72,4 +87,4 @@ func TestValidateNuGet_RealPackages(t *testing.T) {
7287
}
7388
})
7489
}
75-
}
90+
}

0 commit comments

Comments
 (0)