Skip to content

Commit 582d93a

Browse files
sridharavinashtoby
authored andcommitted
feat: implement publish endpoint and related database functionality
1 parent b09275f commit 582d93a

File tree

11 files changed

+417
-30
lines changed

11 files changed

+417
-30
lines changed

internal/api/handlers/v0/publish.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Package v0 contains API handlers for version 0 of the API
2+
package v0
3+
4+
import (
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
9+
"github.com/modelcontextprotocol/registry/internal/model"
10+
"github.com/modelcontextprotocol/registry/internal/service"
11+
)
12+
13+
// PublishHandler handles requests to publish new server details to the registry
14+
func PublishHandler(registry service.RegistryService) http.HandlerFunc {
15+
return func(w http.ResponseWriter, r *http.Request) {
16+
// Only allow POST method
17+
if r.Method != http.MethodPost {
18+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19+
return
20+
}
21+
22+
// Read the request body
23+
body, err := io.ReadAll(r.Body)
24+
if err != nil {
25+
http.Error(w, "Error reading request body", http.StatusBadRequest)
26+
return
27+
}
28+
defer r.Body.Close()
29+
30+
// Parse request body into ServerDetail struct
31+
var serverDetail model.ServerDetail
32+
err = json.Unmarshal(body, &serverDetail)
33+
if err != nil {
34+
http.Error(w, "Invalid request payload", http.StatusBadRequest)
35+
return
36+
}
37+
38+
// Validate required fields
39+
if serverDetail.Name == "" {
40+
http.Error(w, "Name is required", http.StatusBadRequest)
41+
return
42+
}
43+
44+
// version is required
45+
if serverDetail.VersionDetail.Version == "" {
46+
http.Error(w, "Version is required", http.StatusBadRequest)
47+
return
48+
}
49+
50+
// Call the publish method on the registry service
51+
err = registry.Publish(&serverDetail)
52+
if err != nil {
53+
http.Error(w, "Failed to publish server details: "+err.Error(), http.StatusInternalServerError)
54+
return
55+
}
56+
57+
w.Header().Set("Content-Type", "application/json")
58+
w.WriteHeader(http.StatusCreated)
59+
json.NewEncoder(w).Encode(map[string]string{
60+
"message": "Server publication successful",
61+
"id": serverDetail.ID,
62+
})
63+
}
64+
}

internal/api/router/v0.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func RegisterV0Routes(mux *http.ServeMux, cfg *config.Config, registry service.R
1616
mux.HandleFunc("/v0/servers", v0.ServersHandler(registry))
1717
mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry))
1818
mux.HandleFunc("/v0/ping", v0.PingHandler(cfg))
19+
mux.HandleFunc("/v0/publish", v0.PublishHandler(registry))
1920

2021
// Register Swagger UI routes
2122
mux.HandleFunc("/v0/swagger/", v0.SwaggerHandler())

internal/database/database.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type Database interface {
2121
List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error)
2222
// GetByID retrieves a single ServerDetail by it's ID
2323
GetByID(ctx context.Context, id string) (*model.ServerDetail, error)
24+
// Publish adds a new ServerDetail to the database
25+
Publish(ctx context.Context, serverDetail *model.ServerDetail) error
2426
// Close closes the database connection
2527
Close() error
2628
}

internal/database/memory.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (db *MemoryDB) List(ctx context.Context, filter map[string]interface{}, cur
6363
include = false
6464
}
6565
case "version":
66-
if entry.Version != value.(string) {
66+
if entry.VersionDetail.Version != value.(string) {
6767
include = false
6868
}
6969
// Add more filter options as needed
@@ -125,17 +125,54 @@ func (db *MemoryDB) GetByID(ctx context.Context, id string) (*model.ServerDetail
125125

126126
if entry, exists := db.entries[id]; exists {
127127
return &model.ServerDetail{
128-
ID: entry.ID,
129-
Name: entry.Name,
130-
Description: entry.Description,
131-
Version: entry.Version,
132-
Repository: entry.Repository,
128+
ID: entry.ID,
129+
Name: entry.Name,
130+
Description: entry.Description,
131+
VersionDetail: entry.VersionDetail,
132+
Repository: entry.Repository,
133133
}, nil
134134
}
135135

136136
return nil, ErrNotFound
137137
}
138138

139+
// Publish adds a new ServerDetail to the database
140+
func (db *MemoryDB) Publish(ctx context.Context, serverDetail *model.ServerDetail) error {
141+
if ctx.Err() != nil {
142+
return ctx.Err()
143+
}
144+
145+
db.mu.Lock()
146+
defer db.mu.Unlock()
147+
148+
// check for name
149+
if serverDetail.Name == "" {
150+
return ErrInvalidInput
151+
}
152+
153+
// check that the name and the version are unique
154+
155+
for _, entry := range db.entries {
156+
if entry.Name == serverDetail.Name && entry.VersionDetail.Version == serverDetail.VersionDetail.Version {
157+
return ErrAlreadyExists
158+
}
159+
}
160+
161+
if serverDetail.Repository.URL == "" {
162+
return ErrInvalidInput
163+
}
164+
165+
db.entries[serverDetail.ID] = &model.Entry{
166+
ID: serverDetail.ID,
167+
Name: serverDetail.Name,
168+
Description: serverDetail.Description,
169+
VersionDetail: serverDetail.VersionDetail,
170+
Repository: serverDetail.Repository,
171+
}
172+
173+
return nil
174+
}
175+
139176
// Close closes the database connection
140177
// For an in-memory database, this is a no-op
141178
func (db *MemoryDB) Close() error {

internal/database/mongo.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName
4040
// Create indexes for better query performance
4141
models := []mongo.IndexModel{
4242
{
43-
Keys: bson.D{{Key: "name", Value: 1}},
44-
Options: options.Index().SetUnique(true),
43+
Keys: bson.D{{Key: "name", Value: 1}},
4544
},
4645
{
4746
Keys: bson.D{{Key: "id", Value: 1}},
4847
Options: options.Index().SetUnique(true),
4948
},
49+
// add an index for the combination of name and version
50+
{
51+
Keys: bson.D{{Key: "name", Value: 1}, {Key: "versiondetail.version", Value: 1}},
52+
Options: options.Index().SetUnique(true),
53+
},
5054
}
5155

5256
_, err = collection.Indexes().CreateMany(ctx, models)
@@ -73,7 +77,9 @@ func (db *MongoDB) List(ctx context.Context, filter map[string]interface{}, curs
7377
}
7478

7579
// Convert Go map to MongoDB filter
76-
mongoFilter := bson.M{}
80+
mongoFilter := bson.M{
81+
"versiondetail.islatest": true,
82+
}
7783
// Map common filter keys to MongoDB document paths
7884
for k, v := range filter {
7985
// Handle nested fields with dot notation
@@ -164,6 +170,53 @@ func (db *MongoDB) GetByID(ctx context.Context, id string) (*model.ServerDetail,
164170
return &entry, nil
165171
}
166172

173+
// Publish adds a new ServerDetail to the database
174+
func (db *MongoDB) Publish(ctx context.Context, serverDetail *model.ServerDetail) error {
175+
if ctx.Err() != nil {
176+
return ctx.Err()
177+
}
178+
// find a server detail with the same name and check that the current version is greater than the existing one
179+
filter := bson.M{
180+
"name": serverDetail.Name,
181+
"versiondetail.islatest": true,
182+
}
183+
184+
var existingEntry model.ServerDetail
185+
err := db.collection.FindOne(ctx, filter).Decode(&existingEntry)
186+
if err != nil && err != mongo.ErrNoDocuments {
187+
return fmt.Errorf("error checking existing entry: %w", err)
188+
}
189+
190+
// check that the current version is greater than the existing one
191+
if serverDetail.VersionDetail.Version <= existingEntry.VersionDetail.Version {
192+
return fmt.Errorf("version must be greater than existing version")
193+
}
194+
195+
// update the existing entry to not be the latest version
196+
if existingEntry.ID != "" {
197+
_, err = db.collection.UpdateOne(ctx, bson.M{"id": existingEntry.ID}, bson.M{"$set": bson.M{"versiondetail.islatest": false}})
198+
if err != nil {
199+
return fmt.Errorf("error updating existing entry: %w", err)
200+
}
201+
202+
}
203+
204+
serverDetail.ID = uuid.New().String()
205+
serverDetail.VersionDetail.IsLatest = true
206+
serverDetail.VersionDetail.ReleaseDate = time.Now().Format(time.RFC3339)
207+
208+
// Insert the entry into the database
209+
_, err = db.collection.InsertOne(ctx, serverDetail)
210+
if err != nil {
211+
if mongo.IsDuplicateKeyError(err) {
212+
return ErrAlreadyExists
213+
}
214+
return fmt.Errorf("error inserting entry: %w", err)
215+
}
216+
217+
return nil
218+
}
219+
167220
// Close closes the database connection
168221
func (db *MongoDB) Close() error {
169222
return db.client.Disconnect(context.Background())

internal/model/model.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package model
22

33
type Entry struct {
4-
ID string `json:"id,omitempty"`
5-
Name string `json:"name,omitempty"`
6-
Description string `json:"description,omitempty"`
7-
Repository Repository `json:"repository,omitempty"`
8-
Version string `json:"version,omitempty"`
4+
ID string `json:"id,omitempty"`
5+
Name string `json:"name,omitempty"`
6+
Description string `json:"description,omitempty"`
7+
Repository Repository `json:"repository,omitempty"`
8+
VersionDetail VersionDetail `json:"version_detail,omitempty"`
99
}
1010

1111
type Repository struct {
@@ -14,16 +14,21 @@ type Repository struct {
1414
Branch string `json:"branch,omitempty"`
1515
Commit string `json:"commit,omitempty"`
1616
}
17+
type VersionDetail struct {
18+
Version string `json:"version,omitempty"`
19+
ReleaseDate string `json:"release_date,omitempty"` //RFC 3339 date format
20+
IsLatest bool `json:"is_latest,omitempty"`
21+
}
1722

1823
type ServerDetail struct {
19-
ID string `json:"id,omitempty"`
20-
Name string `json:"name,omitempty"`
21-
Description string `json:"description,omitempty"`
22-
Version string `json:"version,omitempty"`
23-
Repository Repository `json:"repository,omitempty"`
24-
RegistryCanonical string `json:"registry_canonical,omitempty"`
25-
Registries []Registries `json:"registries,omitempty"`
26-
Remotes []Remotes `json:"remotes,omitempty"`
24+
ID string `json:"id,omitempty"`
25+
Name string `json:"name,omitempty"`
26+
Description string `json:"description,omitempty"`
27+
VersionDetail VersionDetail `json:"version_detail,omitempty"`
28+
Repository Repository `json:"repository,omitempty"`
29+
RegistryCanonical string `json:"registry_canonical,omitempty"`
30+
Registries []Registries `json:"registries,omitempty"`
31+
Remotes []Remotes `json:"remotes,omitempty"`
2732
}
2833

2934
type Registries struct {

internal/service/fake_service.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ func NewFakeRegistryService() RegistryService {
2828
Branch: "main",
2929
Commit: "abc123def456",
3030
},
31-
Version: "1.0.0",
31+
VersionDetail: model.VersionDetail{
32+
Version: "1.0.0",
33+
ReleaseDate: time.Now().Format(time.RFC3339),
34+
IsLatest: true,
35+
},
3236
},
3337
{
3438
ID: uuid.New().String(),
@@ -40,7 +44,11 @@ func NewFakeRegistryService() RegistryService {
4044
Branch: "develop",
4145
Commit: "def456ghi789",
4246
},
43-
Version: "2.1.0",
47+
VersionDetail: model.VersionDetail{
48+
Version: "0.9.0",
49+
ReleaseDate: time.Now().Format(time.RFC3339),
50+
IsLatest: false,
51+
},
4452
},
4553
{
4654
ID: uuid.New().String(),
@@ -52,7 +60,11 @@ func NewFakeRegistryService() RegistryService {
5260
Branch: "feature/mcp-server",
5361
Commit: "789jkl012mno",
5462
},
55-
Version: "0.9.5",
63+
VersionDetail: model.VersionDetail{
64+
Version: "0.9.5",
65+
ReleaseDate: time.Now().Format(time.RFC3339),
66+
IsLatest: false,
67+
},
5668
},
5769
}
5870

@@ -103,6 +115,16 @@ func (s *fakeRegistryService) GetByID(id string) (*model.ServerDetail, error) {
103115
return serverDetail, nil
104116
}
105117

118+
// Publish adds a new server detail to the in-memory database
119+
func (s *fakeRegistryService) Publish(serverDetail *model.ServerDetail) error {
120+
// Create a timeout context for the database operation
121+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
122+
defer cancel()
123+
124+
// Use the database's Publish method to add the server detail
125+
return s.db.Publish(ctx, serverDetail)
126+
}
127+
106128
// Close closes the in-memory database connection
107129
func (s *fakeRegistryService) Close() error {
108130
return s.db.Close()

internal/service/registry_service.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,21 @@ func (s *registryServiceImpl) GetByID(id string) (*model.ServerDetail, error) {
8181

8282
return serverDetail, nil
8383
}
84+
85+
// Publish adds a new server detail to the registry
86+
func (s *registryServiceImpl) Publish(serverDetail *model.ServerDetail) error {
87+
// Create a timeout context for the database operation
88+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
89+
defer cancel()
90+
91+
if serverDetail == nil {
92+
return database.ErrInvalidInput
93+
}
94+
95+
err := s.db.Publish(ctx, serverDetail)
96+
if err != nil {
97+
return err
98+
}
99+
100+
return nil
101+
}

internal/service/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ import "github.com/modelcontextprotocol/registry/internal/model"
66
type RegistryService interface {
77
List(cursor string, limit int) ([]model.Entry, string, error)
88
GetByID(id string) (*model.ServerDetail, error)
9+
Publish(serverDetail *model.ServerDetail) error
910
}

0 commit comments

Comments
 (0)