diff --git a/.env.example b/.env.example index ba9eeeb..4dee64c 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ # Server configuration MCP_REGISTRY_SERVER_ADDRESS=:8080 +MCP_REGISTRY_VERSION=dev +MCP_REGISTRY_LOG_LEVEL=info # Database configuration MCP_REGISTRY_DATABASE_TYPE=mongodb @@ -9,20 +11,21 @@ MCP_REGISTRY_DATABASE_URL=mongodb://username:password@localhost:27017 MCP_REGISTRY_DATABASE_NAME=mcp-registry MCP_REGISTRY_COLLECTION_NAME=servers_v2 -# Application configuration -MCP_REGISTRY_LOG_LEVEL=info -MCP_REGISTRY_SEED_FILE_PATH=data/seed.json -MCP_REGISTRY_SEED_IMPORT=true -MCP_REGISTRY_VERSION=dev +# Path or URL to import seed data (supports local files and HTTP URLs) +MCP_REGISTRY_SEED_FROM=data/seed.json # GitHub OAuth configuration -MCP_REGISTRY_GITHUB_CLIENT_ID= -MCP_REGISTRY_GITHUB_CLIENT_SECRET= +# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App +# They don't provide any real privileged access, hence why it's okay that they're here +# The staging and prod credentials client secrets are sensitive and are stored in encrypted form in ./deploy +MCP_REGISTRY_GITHUB_CLIENT_ID=Iv23licy3GSiM9Km5jtd +MCP_REGISTRY_GITHUB_CLIENT_SECRET=0e8db54879b02c29adef51795586f3c510a9341d # JWT configuration # This should be a 32-byte Ed25519 seed (not the full private key). Generate a new seed with: `openssl rand -hex 32` MCP_REGISTRY_JWT_PRIVATE_KEY=bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c -# Anonymous authentication (for development/testing only) +# Anonymous authentication for development/testing only # When enabled, allows anyone to get tokens for publishing to io.modelcontextprotocol.anonymous/* namespace +# This should be disabled in prod MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=false \ No newline at end of file diff --git a/README.md b/README.md index 36685da..ae922af 100644 --- a/README.md +++ b/README.md @@ -204,20 +204,7 @@ The API is documented using Swagger/OpenAPI. This page provides a complete refer ## Configuration -The service can be configured using environment variables: - -| Variable | Description | Default | -|----------|-------------|---------| -| `MCP_REGISTRY_APP_VERSION` | Application version | `dev` | -| `MCP_REGISTRY_DATABASE_TYPE` | Database type | `mongodb` | -| `MCP_REGISTRY_COLLECTION_NAME` | MongoDB collection name | `servers_v2` | -| `MCP_REGISTRY_DATABASE_NAME` | MongoDB database name | `mcp-registry` | -| `MCP_REGISTRY_DATABASE_URL` | MongoDB connection string | `mongodb://localhost:27017` | -| `MCP_REGISTRY_GITHUB_CLIENT_ID` | GitHub App Client ID | | -| `MCP_REGISTRY_GITHUB_CLIENT_SECRET` | GitHub App Client Secret | | -| `MCP_REGISTRY_LOG_LEVEL` | Log level | `info` | -| `MCP_REGISTRY_SEED_FROM` | Path or URL to import seed data (supports local files and HTTP URLs) | `data/seed.json` | -| `MCP_REGISTRY_SERVER_ADDRESS` | Listen address for the server | `:8080` | +The service can be configured using environment variables. See [.env.example](./.env.example) for details. ## Pre-built Docker Images diff --git a/deploy/Pulumi.gcpProd.yaml b/deploy/Pulumi.gcpProd.yaml index 11d61f9..af198a5 100644 --- a/deploy/Pulumi.gcpProd.yaml +++ b/deploy/Pulumi.gcpProd.yaml @@ -2,9 +2,9 @@ config: mcp-registry:environment: prod mcp-registry:provider: gcp gcp:project: mcp-registry-prod - mcp-registry:githubClientId: your-github-client-id + mcp-registry:githubClientId: Iv23liUydBbI7Z2Q9bOZ mcp-registry:githubClientSecret: - secure: v1:68BmTaKlZ+v77IDj:vVeMsAq/8QoCzkLzgo8MC8F5ALcC5TydWiqZ2NmnnFRpfF3MRPV69Os= + secure: v1:mSVikc0wDjoN8jCF:ytoI2gZ5WRJN3Fd6s5SRd2fnzirqtFBPSdIshIa2RfnF0OdtDpmucfs5KRw3HoJVGfDTbkrG+Sk= gcp:credentials: secure: v1:hyZWlpeMTFDnMcz2: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 mcp-registry:jwtPrivateKey: diff --git a/deploy/Pulumi.gcpStaging.yaml b/deploy/Pulumi.gcpStaging.yaml index 0553422..52c3492 100644 --- a/deploy/Pulumi.gcpStaging.yaml +++ b/deploy/Pulumi.gcpStaging.yaml @@ -2,9 +2,9 @@ config: mcp-registry:environment: staging mcp-registry:provider: gcp gcp:project: mcp-registry-staging - mcp-registry:githubClientId: your-github-client-id + mcp-registry:githubClientId: Iv23liB2r5oUw6PxiMOC mcp-registry:githubClientSecret: - secure: v1:BpUPXf7aQcG8qHF9:YJxKbhWi5ZQSTdNGrdhzzCGz7sWXKhmNe+yrTls2cKC3/w== + secure: v1:WRAquCFbMSoG7+iD:LCiTHniEP6eCHeE440q+lI/KqyYvK+0gZ4s4NseM1pN9JgWFt31wRttiQ7nWeHQpf0QEkZFALZY= gcp:credentials: secure: v1:RaHpGsBp37XO/EhJ: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 mcp-registry:jwtPrivateKey: diff --git a/deploy/Pulumi.local.yaml b/deploy/Pulumi.local.yaml index 57d3bc9..f179b79 100644 --- a/deploy/Pulumi.local.yaml +++ b/deploy/Pulumi.local.yaml @@ -1,7 +1,6 @@ config: mcp-registry:environment: local mcp-registry:provider: local - mcp-registry:githubClientId: test-client-id - mcp-registry:githubClientSecret: - secure: v1:gt5MBuW7QPiJymkh:1+I2eFChsrUH18cLELq9OAIN94MLH0SldbOuPp2C + mcp-registry:githubClientId: Iv23licy3GSiM9Km5jtd + mcp-registry:githubClientSecret: 0e8db54879b02c29adef51795586f3c510a9341d encryptionsalt: v1:ijIHaqhbXVA=:v1:7voX1Kv+Bunz33iN:fyVHMOhlGIymzJ+ILgUBy3ExTwUUnA== diff --git a/docker-compose.yml b/docker-compose.yml index 53752a2..f7ef51a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,12 @@ services: dockerfile: Dockerfile depends_on: - mongodb + # See .env.example for more documentation environment: - MCP_REGISTRY_DATABASE_URL=${MCP_REGISTRY_DATABASE_URL:-mongodb://mongodb:27017} - MCP_REGISTRY_ENVIRONMENT=${MCP_REGISTRY_ENVIRONMENT:-test} - - MCP_REGISTRY_GITHUB_CLIENT_ID=${MCP_REGISTRY_GITHUB_CLIENT_ID} - - MCP_REGISTRY_GITHUB_CLIENT_SECRET=${MCP_REGISTRY_GITHUB_CLIENT_SECRET} + - MCP_REGISTRY_GITHUB_CLIENT_ID=${MCP_REGISTRY_GITHUB_CLIENT_ID:-Iv23licy3GSiM9Km5jtd} + - MCP_REGISTRY_GITHUB_CLIENT_SECRET=${MCP_REGISTRY_GITHUB_CLIENT_SECRET:-0e8db54879b02c29adef51795586f3c510a9341d} - MCP_REGISTRY_JWT_PRIVATE_KEY=${MCP_REGISTRY_JWT_PRIVATE_KEY:-8103179d8ef955f6d3de6d6217224a909ec4060529dfeb1d4ca5a994537658cd} - MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=${MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH:-true} ports: diff --git a/internal/api/handlers/v0/auth/github.go b/internal/api/handlers/v0/auth/github.go index 872c951..d94b2ea 100644 --- a/internal/api/handlers/v0/auth/github.go +++ b/internal/api/handlers/v0/auth/github.go @@ -79,7 +79,7 @@ func (h *GitHubHandler) ExchangeToken(ctx context.Context, githubToken string) ( } // Get user's organizations - orgs, err := h.getGitHubUserOrgs(ctx, githubToken) + orgs, err := h.getGitHubUserOrgs(ctx, user.Login, githubToken) if err != nil { return nil, fmt.Errorf("failed to get GitHub organizations: %w", err) } @@ -138,9 +138,8 @@ func (h *GitHubHandler) getGitHubUser(ctx context.Context, token string) (*GitHu return &user, nil } -// getGitHubUserOrgs gets the authenticated user's organizations -func (h *GitHubHandler) getGitHubUserOrgs(ctx context.Context, token string) ([]GitHubUserOrOrg, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.baseURL+"/user/orgs", nil) +func (h *GitHubHandler) getGitHubUserOrgs(ctx context.Context, username string, token string) ([]GitHubUserOrOrg, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.baseURL+"/users/"+username+"/orgs", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/api/handlers/v0/auth/github_test.go b/internal/api/handlers/v0/auth/github_test.go index 976694a..2403d24 100644 --- a/internal/api/handlers/v0/auth/github_test.go +++ b/internal/api/handlers/v0/auth/github_test.go @@ -23,7 +23,7 @@ import ( const ( githubUserEndpoint = "/user" - githubOrgsEndpoint = "/user/orgs" + githubOrgsEndpoint = "/users/testuser/orgs" ) func TestGitHubHandler_ExchangeToken(t *testing.T) { @@ -226,7 +226,7 @@ func TestGitHubHandler_ExchangeToken(t *testing.T) { } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) //nolint:errcheck - case githubOrgsEndpoint: + case "/users/user with spaces/orgs": orgs := []v0auth.GitHubUserOrOrg{} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(orgs) //nolint:errcheck @@ -259,7 +259,7 @@ func TestGitHubHandler_ExchangeToken(t *testing.T) { switch r.URL.Path { case githubUserEndpoint: user := v0auth.GitHubUserOrOrg{ - Login: "validuser", + Login: "testuser", ID: 12345, } w.Header().Set("Content-Type", "application/json") @@ -290,7 +290,7 @@ func TestGitHubHandler_ExchangeToken(t *testing.T) { jwtManager := auth.NewJWTManager(cfg) claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken) require.NoError(t, err) - assert.Equal(t, "validuser", claims.AuthMethodSubject) + assert.Equal(t, "testuser", claims.AuthMethodSubject) assert.Empty(t, claims.Permissions) // No permissions because one org has invalid name }) @@ -538,7 +538,7 @@ func TestValidGitHubNames(t *testing.T) { } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) //nolint:errcheck - case githubOrgsEndpoint: + case "/users/" + tc.username + "/orgs": w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tc.orgs) //nolint:errcheck } @@ -596,7 +596,7 @@ func TestConcurrentTokenExchange(t *testing.T) { } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) //nolint:errcheck - case "/user/orgs": + case githubOrgsEndpoint: orgs := []v0auth.GitHubUserOrOrg{} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(orgs) //nolint:errcheck diff --git a/internal/config/config.go b/internal/config/config.go index 91560e3..d0bb12b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,6 +12,7 @@ const ( ) // Config holds the application configuration +// See .env.example for more documentation type Config struct { ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` DatabaseType DatabaseType `env:"DATABASE_TYPE" envDefault:"mongodb"` diff --git a/tests/integration/main.go b/tests/integration/main.go index 76a9dc1..f2017d8 100644 --- a/tests/integration/main.go +++ b/tests/integration/main.go @@ -85,10 +85,10 @@ func run() error { log.Fatalf("failed to get anonymous token: %v", err) } - if err := os.WriteFile(".mcpregistry_token", []byte(token), 0600); err != nil { + if err := os.WriteFile(".mcpregistry_registry_token", []byte(token), 0600); err != nil { log.Fatalf("failed to write token: %v", err) } - defer os.Remove(".mcpregistry_token") + defer os.Remove(".mcpregistry_registry_token") return publish(examples) } @@ -121,7 +121,7 @@ func publish(examples []example) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "./bin/publisher", "publish", "--mcp-file", p, "--registry-url", registryURL) + cmd := exec.CommandContext(ctx, "./bin/publisher", "publish", "--mcp-file", p, "--registry-url", registryURL, "--auth-method", "none") cmd.WaitDelay = 100 * time.Millisecond out, err := cmd.CombinedOutput() diff --git a/tests/integration/run.sh b/tests/integration/run.sh index 0943b75..f785a34 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -15,8 +15,6 @@ docker build -t registry . trap cleanup EXIT -export MCP_REGISTRY_GITHUB_CLIENT_ID=fake -export MCP_REGISTRY_GITHUB_CLIENT_SECRET=fake docker compose -f docker-compose.yml -f tests/integration/docker-compose.integration-test.yml up --wait --wait-timeout 60 go run tests/integration/main.go diff --git a/tools/publisher/README.md b/tools/publisher/README.md index 114611c..ad051ca 100644 --- a/tools/publisher/README.md +++ b/tools/publisher/README.md @@ -42,7 +42,7 @@ The tool supports two main commands: - `-registry-url`: URL of the MCP registry (required) - `-mcp-file`: Path to the MCP configuration file (required) - `-login`: Force a new GitHub authentication even if a token already exists (overwrites existing token file) -- `-auth-method`: Authentication method to use (default: github-oauth) +- `-auth-method`: Authentication method to use (default: github) ## Creating a server.json file @@ -132,8 +132,9 @@ The tool has been simplified to use **GitHub OAuth device flow authentication ex 1. **Automatic Setup**: The tool automatically retrieves the GitHub Client ID from the registry's health endpoint 2. **First Run Authentication**: When first run (or with the `--login` flag), the tool initiates the GitHub device flow 3. **User Authorization**: You'll be provided with a URL and a verification code to enter on GitHub -4. **Token Storage**: After successful authentication, the tool saves the access token locally in `.mcpregistry_token` for future use -5. **Secure Communication**: The token is sent in the HTTP Authorization header with the Bearer scheme for all registry API calls +4. **Token Storage**: After successful authentication, the tool saves the access token locally in `.mcpregistry_github_token` for future use +5. **Token Exchange**: The GitHub token is exchanged for a short-lived registry token, which is saved locally in `.mcpregistry_registry_token`. +5. **Secure Communication**: The registry token is sent in the HTTP Authorization header with the Bearer scheme for all registry API calls **Note**: Authentication is performed via GitHub OAuth App, which you must authorize for the respective resources (e.g., organization access if publishing organization repositories). @@ -141,46 +142,7 @@ The tool has been simplified to use **GitHub OAuth device flow authentication ex To publish an existing server.json file to the registry: -1. Prepare your `server.json` file with your server details: - -```json -{ - "name": "io.github.yourusername/your-repository", - "description": "Your MCP server description", - "version_detail": { - "version": "1.0.0" - }, - "packages": [ - { - "registry_name": "npm", - "name": "your-npm-package", - "version": "1.0.0", - "package_arguments": [ - { - "description": "Specify services and permissions", - "is_required": true, - "format": "string", - "value": "-s", - "default": "-s", - "type": "positional", - "value_hint": "-s" - } - ], - "environment_variables": [ - { - "name": "API_KEY", - "description": "API Key to access the server" - } - ] - } - ], - "repository": { - "url": "https://github.com/yourusername/your-repository", - "source": "github" - } -} -``` - +1. Prepare your `server.json` file with your server details. See an example in [server.json](./server.json) 2. Run the publisher tool: ```bash @@ -190,12 +152,3 @@ To publish an existing server.json file to the registry: 3. Follow the authentication instructions in the terminal if prompted. 4. Upon successful publication, you'll see a confirmation message. - -## Important Notes - -- **GitHub Authentication Only**: The tool exclusively uses GitHub OAuth device flow for authentication -- **Automatic Client ID**: The GitHub Client ID is automatically retrieved from the registry's health endpoint -- **Token Storage**: The authentication token is saved in `.mcpregistry_token` in the current directory -- **Internet Required**: Active internet connection needed for GitHub authentication and registry communication -- **Repository Access**: Ensure the repository and package mentioned in your `server.json` file exist and are accessible -- **OAuth Permissions**: You may need to grant the OAuth app access to your GitHub organizations if publishing org repositories diff --git a/tools/publisher/auth/README.md b/tools/publisher/auth/README.md index 4f0e3af..f8069c7 100644 --- a/tools/publisher/auth/README.md +++ b/tools/publisher/auth/README.md @@ -79,7 +79,7 @@ The main application automatically selects the appropriate authentication provid // Create the appropriate auth provider based on configuration var authProvider auth.Provider switch authMethod { -case "github-oauth": +case "github": log.Println("Using GitHub OAuth for authentication") authProvider = github.NewOAuthProvider(forceLogin, registryURL) default: diff --git a/tools/publisher/auth/github/oauth.go b/tools/publisher/auth/github/oauth.go index d1e5afc..bf8027c 100644 --- a/tools/publisher/auth/github/oauth.go +++ b/tools/publisher/auth/github/oauth.go @@ -13,7 +13,8 @@ import ( ) const ( - tokenFilePath = ".mcpregistry_token" // #nosec:G101 + gitHubTokenFilePath = ".mcpregistry_github_token" // #nosec:G101 + registryTokenFilePath = ".mcpregistry_registry_token" // #nosec:G101 // GitHub OAuth URLs GitHubDeviceCodeURL = "https://github.com/login/device/code" // #nosec:G101 GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101 @@ -36,6 +37,18 @@ type AccessTokenResponse struct { Error string `json:"error,omitempty"` } +// RegistryTokenResponse represents the response from registry's token exchange endpoint +type RegistryTokenResponse struct { + RegistryToken string `json:"registry_token"` + ExpiresAt int64 `json:"expires_at"` +} + +// StoredRegistryToken represents the registry token with expiration stored locally +type StoredRegistryToken struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` +} + // OAuthProvider implements the AuthProvider interface using GitHub's device flow type OAuthProvider struct { clientID string @@ -57,9 +70,33 @@ func NewOAuthProvider(forceLogin bool, registryURL string) *OAuthProvider { } } -// GetToken retrieves the GitHub access token -func (g *OAuthProvider) GetToken(_ context.Context) (string, error) { - return readToken() +// GetToken retrieves the registry JWT token (exchanges GitHub token if needed) +func (g *OAuthProvider) GetToken(ctx context.Context) (string, error) { + // Check if we have a valid registry token + registryToken, err := readRegistryToken() + if err == nil && registryToken != "" { + return registryToken, nil + } + + // If no valid registry token, exchange GitHub token for registry token + githubToken, err := readToken() + if err != nil { + return "", fmt.Errorf("failed to read GitHub token: %w", err) + } + + // Exchange GitHub token for registry token + registryToken, expiresAt, err := g.exchangeTokenForRegistry(ctx, githubToken) + if err != nil { + return "", fmt.Errorf("failed to exchange token: %w", err) + } + + // Store the registry token + err = saveRegistryToken(registryToken, expiresAt) + if err != nil { + return "", fmt.Errorf("failed to save registry token: %w", err) + } + + return registryToken, nil } // NeedsLogin checks if a new login is required @@ -68,8 +105,21 @@ func (g *OAuthProvider) NeedsLogin() bool { return true } - _, statErr := os.Stat(tokenFilePath) - return os.IsNotExist(statErr) + // Check if GitHub token exists + _, statErr := os.Stat(gitHubTokenFilePath) + if os.IsNotExist(statErr) { + return true + } + + // Check if valid registry token exists + _, err := readRegistryToken() + if err != nil { + // No valid registry token, but we have GitHub token + // We don't need to login, just exchange tokens + return false + } + + return false } // Login performs the GitHub device flow authentication @@ -115,7 +165,7 @@ func (g *OAuthProvider) Login(ctx context.Context) error { // Name returns the name of this auth provider func (g *OAuthProvider) Name() string { - return "github-oauth" + return "github" } // requestDeviceCode initiates the device authorization flow @@ -237,12 +287,12 @@ func (g *OAuthProvider) pollForToken(ctx context.Context, deviceCode string) (st // saveToken saves the GitHub access token to a local file func saveToken(token string) error { - return os.WriteFile(tokenFilePath, []byte(token), 0600) + return os.WriteFile(gitHubTokenFilePath, []byte(token), 0600) } // readToken reads the GitHub access token from a local file func readToken() (string, error) { - tokenData, err := os.ReadFile(tokenFilePath) + tokenData, err := os.ReadFile(gitHubTokenFilePath) if err != nil { return "", err } @@ -292,3 +342,91 @@ func getClientID(ctx context.Context, registryURL string) (string, error) { return githubClientID, nil } + +// exchangeTokenForRegistry exchanges a GitHub token for a registry JWT token +func (g *OAuthProvider) exchangeTokenForRegistry(ctx context.Context, githubToken string) (string, int64, error) { + if g.registryURL == "" { + return "", 0, fmt.Errorf("registry URL is required for token exchange") + } + + // Prepare the request body + payload := map[string]string{ + "github_token": githubToken, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return "", 0, fmt.Errorf("failed to marshal request: %w", err) + } + + // Make the token exchange request + exchangeURL := g.registryURL + "/v0/auth/github" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData)) + if err != nil { + return "", 0, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", 0, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", 0, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", 0, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body) + } + + var tokenResp RegistryTokenResponse + err = json.Unmarshal(body, &tokenResp) + if err != nil { + return "", 0, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return tokenResp.RegistryToken, tokenResp.ExpiresAt, nil +} + +// saveRegistryToken saves the registry JWT token to a local file with expiration +func saveRegistryToken(token string, expiresAt int64) error { + storedToken := StoredRegistryToken{ + Token: token, + ExpiresAt: expiresAt, + } + + data, err := json.Marshal(storedToken) + if err != nil { + return fmt.Errorf("failed to marshal token: %w", err) + } + + return os.WriteFile(registryTokenFilePath, data, 0600) +} + +// readRegistryToken reads the registry JWT token from a local file +func readRegistryToken() (string, error) { + data, err := os.ReadFile(registryTokenFilePath) + if err != nil { + return "", err + } + + var storedToken StoredRegistryToken + err = json.Unmarshal(data, &storedToken) + if err != nil { + return "", fmt.Errorf("failed to unmarshal token: %w", err) + } + + // Check if token has expired + if time.Now().Unix() >= storedToken.ExpiresAt { + // Token has expired, remove the file + os.Remove(registryTokenFilePath) + return "", fmt.Errorf("registry token has expired") + } + + return storedToken.Token, nil +} diff --git a/tools/publisher/auth/none/none.go b/tools/publisher/auth/none/none.go new file mode 100644 index 0000000..6200dfb --- /dev/null +++ b/tools/publisher/auth/none/none.go @@ -0,0 +1,78 @@ +package none + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/modelcontextprotocol/registry/tools/publisher/auth" +) + +type Provider struct { + registryURL string + token string +} + +type TokenResponse struct { + RegistryToken string `json:"registry_token"` + ExpiresAt int64 `json:"expires_at"` +} + +func NewProvider(registryURL string) auth.Provider { //nolint:ireturn + return &Provider{ + registryURL: registryURL, + } +} + +func (p *Provider) GetToken(ctx context.Context) (string, error) { + if p.token != "" { + return p.token, nil + } + + // Get anonymous token from registry + if !strings.HasSuffix(p.registryURL, "/") { + p.registryURL += "/" + } + tokenURL := p.registryURL + "v0/auth/none" + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %w", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error getting anonymous token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("failed to get anonymous token (status %d): %s", resp.StatusCode, body) + } + + var tokenResp TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return "", fmt.Errorf("error decoding token response: %w", err) + } + + p.token = tokenResp.RegistryToken + return p.token, nil +} + +func (p *Provider) NeedsLogin() bool { + return false +} + +func (p *Provider) Login(_ context.Context) error { + // No login needed for anonymous auth + return nil +} + +func (p *Provider) Name() string { + return "none" +} diff --git a/tools/publisher/main.go b/tools/publisher/main.go index f049485..99ddb09 100644 --- a/tools/publisher/main.go +++ b/tools/publisher/main.go @@ -15,6 +15,7 @@ import ( "github.com/modelcontextprotocol/registry/tools/publisher/auth" "github.com/modelcontextprotocol/registry/tools/publisher/auth/github" + "github.com/modelcontextprotocol/registry/tools/publisher/auth/none" ) // Server structure types for JSON generation @@ -103,8 +104,8 @@ func publishCommand() error { publishFlags.StringVar(®istryURL, "registry-url", "", "URL of the registry (required)") publishFlags.StringVar(&mcpFilePath, "mcp-file", "", "path to the MCP file (required)") publishFlags.BoolVar(&forceLogin, "login", false, "force a new login even if a token exists") - publishFlags.StringVar(&authMethod, "auth-method", "github-oauth", - "authentication method to use (default: github-oauth)") + publishFlags.StringVar(&authMethod, "auth-method", "github", + "authentication method (github for authenticated, none for anonymous) (default: github)") // Set custom usage function publishFlags.Usage = func() { @@ -116,7 +117,7 @@ func publishCommand() error { fmt.Fprint(os.Stdout, " --registry-url string URL of the registry (required)\n") fmt.Fprint(os.Stdout, " --mcp-file string path to the MCP file (required)\n") fmt.Fprint(os.Stdout, " --login force a new login even if a token exists\n") - fmt.Fprint(os.Stdout, " --auth-method string authentication method to use (default: github-oauth)\n") + fmt.Fprint(os.Stdout, " --auth-method string authentication method (github for authenticated, none for anonymous) (default: github)\n") } if err := publishFlags.Parse(os.Args[2:]); err != nil { @@ -136,9 +137,12 @@ func publishCommand() error { var authProvider auth.Provider // Determine the authentication method switch authMethod { - case "github-oauth": + case "github": log.Println("Using GitHub OAuth for authentication") authProvider = github.NewOAuthProvider(forceLogin, registryURL) + case "none": + log.Println("Using anonymous authentication") + authProvider = none.NewProvider(registryURL) default: return fmt.Errorf("unsupported authentication method: %s", authMethod) } diff --git a/tools/publisher/server.json b/tools/publisher/server.json index e666d70..b495201 100644 --- a/tools/publisher/server.json +++ b/tools/publisher/server.json @@ -50,7 +50,8 @@ ], "repository": { "url": "https://github.com//", - "source": "github" + "source": "github", + "id": "c8d4c3b6-7943-474d-8e14-b53ab876a56a" }, "version_detail": { "version": "0.0.1-"