feat: implement server publishing endpoint with Bearer token authentication and update documentation

This commit is contained in:
Avinash Sridhar 2025-04-30 12:34:00 -04:00 • committed by Toby Padilla
commit 9c6eebbed0
8 changed files with 190 additions and 48 deletions

View file

@ -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
```

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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" \

View file

@ -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`)

View file

@ -44,10 +44,12 @@ func main() {
var registryURL string
var mcpFilePath string
var forceLogin bool
var providedToken string
flag.StringVar(&registryURL, "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)