diff --git a/README.md b/README.md index 4d2c2d3..eba2aeb 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,53 @@ Response example: } ``` +#### Publish a Server Entry + +``` +POST /v0/publish +``` + +Publishes a new MCP server entry to the registry. Authentication is required via Bearer token in the Authorization header. + +Headers: +- `Authorization`: Bearer token for authentication (e.g., `Bearer your_token_here`) +- `Content-Type`: application/json + +Request body example: +```json +{ + "server_detail": { + "name": "io.github.username/repository", + "description": "Your MCP server description", + "version_detail": { + "version": "1.0.0" + }, + "registries": [ + { + "name": "npm", + "package_name": "your-package-name", + "license": "MIT" + } + ], + "remotes": [ + { + "transport_type": "http", + "url": "https://your-api-endpoint.com" + } + ] + }, + "repo_ref": "username/repository" +} +``` + +Response example: +```json +{ + "message": "Server publication successful", + "id": "1234567890abcdef12345678" +} +``` + ### Ping Endpoint ``` diff --git a/internal/api/handlers/v0/publish.go b/internal/api/handlers/v0/publish.go index 3565d1f..009d664 100644 --- a/internal/api/handlers/v0/publish.go +++ b/internal/api/handlers/v0/publish.go @@ -5,23 +5,13 @@ import ( "encoding/json" "io" "net/http" + "strings" "github.com/modelcontextprotocol/registry/internal/auth" "github.com/modelcontextprotocol/registry/internal/model" "github.com/modelcontextprotocol/registry/internal/service" ) -// httpError represents an HTTP error with a message and status code -type httpError struct { - msg string - status int -} - -// Error returns the error message -func (e *httpError) Error() string { - return e.msg -} - // PublishHandler handles requests to publish new server details to the registry func PublishHandler(registry service.RegistryService, authService auth.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -62,23 +52,39 @@ func PublishHandler(registry service.RegistryService, authService auth.Service) return } - // Validate authentication credentials - if authService != nil { - publishReq.Authentication.RepoRef = serverDetail.Name - valid, err := authService.ValidateAuth(r.Context(), publishReq.Authentication) - if err != nil { - if err == auth.ErrAuthRequired { - http.Error(w, "Authentication is required for publishing", http.StatusUnauthorized) - return - } - http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized) - return - } + // Get auth token from Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header is required", http.StatusUnauthorized) + return + } - if !valid { - http.Error(w, "Invalid authentication credentials", http.StatusUnauthorized) + // Handle bearer token format (e.g., "Bearer xyz123") + token := authHeader + if len(authHeader) > 7 && strings.ToUpper(authHeader[:7]) == "BEARER " { + token = authHeader[7:] + } + + // Setup authentication info + a := model.Authentication{ + Method: model.AuthMethodGitHub, + Token: token, + RepoRef: serverDetail.Name, + } + + valid, err := authService.ValidateAuth(r.Context(), a) + if err != nil { + if err == auth.ErrAuthRequired { + http.Error(w, "Authentication is required for publishing", http.StatusUnauthorized) return } + http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized) + return + } + + if !valid { + http.Error(w, "Invalid authentication credentials", http.StatusUnauthorized) + return } // Call the publish method on the registry service diff --git a/internal/docs/publish_swagger.yaml b/internal/docs/publish_swagger.yaml index 7a49a4d..00d6079 100644 --- a/internal/docs/publish_swagger.yaml +++ b/internal/docs/publish_swagger.yaml @@ -5,6 +5,8 @@ summary: Publish a server to the registry description: Adds a new server to the MCP registry with authentication operationId: publishServer + security: + - bearerAuth: [] requestBody: required: true content: @@ -14,11 +16,9 @@ properties: server_detail: $ref: '#/components/schemas/ServerDetail' - authentication: - $ref: '#/components/schemas/Authentication' - auth_status_token: + repo_ref: type: string - description: Optional status token from a device authorization flow + description: Repository reference used for authentication (defaults to server name if not provided) responses: '201': description: Server published successfully diff --git a/internal/docs/swagger.yaml b/internal/docs/swagger.yaml index ae6ceac..fbd4251 100644 --- a/internal/docs/swagger.yaml +++ b/internal/docs/swagger.yaml @@ -101,6 +101,74 @@ paths: '405': description: Method not allowed + /v0/publish: + post: + tags: + - servers + summary: Publish a server to the registry + description: Adds a new server to the MCP registry with authentication + operationId: publishServer + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + server_detail: + $ref: '#/components/schemas/ServerDetail' + repo_ref: + type: string + description: Repository reference used for authentication (defaults to server name if not provided) + responses: + '201': + description: Server published successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Server publication successful + id: + type: string + example: 1234567890abcdef12345678 + '400': + description: Invalid request + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: Name is required + '401': + description: Authentication failed + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: Authentication is required for publishing + '405': + description: Method not allowed + '500': + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: Failed to publish server details + /v0/servers/{id}: get: tags: @@ -131,6 +199,13 @@ paths: description: Method not allowed components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"" + schemas: Server: type: object diff --git a/internal/model/model.go b/internal/model/model.go index 8ace909..7ebeb12 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -20,7 +20,7 @@ type Authentication struct { // PublishRequest represents a request to publish a server to the registry type PublishRequest struct { ServerDetail ServerDetail `json:"server_detail"` - Authentication Authentication `json:"authentication,omitempty"` + Authentication Authentication `json:"-"` // Now provided via Authorization header AuthStatusToken string `json:"-"` // Used internally for device flows } diff --git a/scripts/test_publish.sh b/scripts/test_publish.sh index d0c7e2c..943fe50 100755 --- a/scripts/test_publish.sh +++ b/scripts/test_publish.sh @@ -115,8 +115,13 @@ echo "Testing publish endpoint: $HOST/v0/publish" response_file=$(mktemp) headers_file=$(mktemp) +# Get token for authentication (or use dummy token for testing) +AUTH_TOKEN=${AUTH_TOKEN:-"test_token"} + # Execute curl with response body to file and headers+status to another file -curl -s -X POST -H "Content-Type: application/json" \ +curl -s -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ -d "@$PAYLOAD_FILE" \ -D "$headers_file" \ -o "$response_file" \ diff --git a/tools/publisher/README.md b/tools/publisher/README.md index 7c49bca..c8b45cb 100644 --- a/tools/publisher/README.md +++ b/tools/publisher/README.md @@ -31,6 +31,7 @@ The compiled binary will be placed in the `bin` directory. - `--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) +- `--token`: Use the provided token instead of GitHub authentication (bypasses the device flow) ## Authentication @@ -38,6 +39,7 @@ The tool uses GitHub device flow authentication: 1. When first run (or with `--login` flag), the tool will initiate the GitHub device flow 2. You'll be provided with a URL and a code to enter 3. After successful authentication, the tool saves the token locally for future use +4. The token is sent in the HTTP Authorization header with the Bearer scheme _NOTE_ : Authentication is made on behalf of a OAuth App which you must authorize for respective resources (e.g `org`) diff --git a/tools/publisher/main.go b/tools/publisher/main.go index 7134624..db77484 100644 --- a/tools/publisher/main.go +++ b/tools/publisher/main.go @@ -44,10 +44,12 @@ func main() { var registryURL string var mcpFilePath string var forceLogin bool + var providedToken string flag.StringVar(®istryURL, "registry-url", "", "URL of the registry(required)") flag.StringVar(&mcpFilePath, "mcp-file", "", "path to the MCP file(required)") flag.BoolVar(&forceLogin, "login", false, "force a new login even if a token exists") + flag.StringVar(&providedToken, "token", "", "use the provided token instead of GitHub authentication") flag.Parse() @@ -56,23 +58,31 @@ func main() { return } - // Check if token exists or force login is requested - _, statErr := os.Stat(tokenFilePath) - if forceLogin || os.IsNotExist(statErr) { - err := performDeviceFlowLogin() + var token string + + // If a token is provided via the command line, use it + if providedToken != "" { + token = providedToken + } else { + // Check if token exists or force login is requested + _, statErr := os.Stat(tokenFilePath) + if forceLogin || os.IsNotExist(statErr) { + err := performDeviceFlowLogin() + if err != nil { + fmt.Printf("Failed to perform device flow login: %s\n", err.Error()) + return + } + } + + // Read the token from the file + var err error + token, err = readToken() if err != nil { - fmt.Printf("Failed to perform device flow login: %s\n", err.Error()) + fmt.Printf("Error reading token: %s\n", err.Error()) return } } - // Read the token - token, err := readToken() - if err != nil { - fmt.Printf("Error reading token: %s\n", err.Error()) - return - } - // Read MCP file mcpData, err := os.ReadFile(mcpFilePath) if err != nil { @@ -255,13 +265,9 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error { return fmt.Errorf("error parsing mcp.json file: %w", err) } - // Create the publish request payload + // Create the publish request payload (without authentication) publishReq := map[string]interface{}{ "server_detail": mcpDetails, - "authentication": map[string]interface{}{ - "method": "github", - "token": token, - }, } // Convert the request to JSON @@ -282,6 +288,7 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, err := client.Do(req)