feat: implement server publishing endpoint with Bearer token authentication and update documentation
This commit is contained in:
parent
e0a4883465
commit
9c6eebbed0
8 changed files with 190 additions and 48 deletions
47
README.md
47
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
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue