## Summary
This PR adds two important improvements to ensure data integrity:
1. **Adds server name format validation at the API layer** to match the
database constraint from migration 008
2. **Includes migration 008 to clean up invalid data** before applying
strict constraints in migration 009
## Changes
### Server Name Validation
- Enforces validation at the API layer matching the database constraint
- **Namespace pattern**: `[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]`
- Must start and end with alphanumeric characters
- Can contain dots and hyphens in the middle
- **Name pattern**: `[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]`
- Must start and end with alphanumeric characters
- Can contain dots, underscores, and hyphens in the middle
- Provides clear, user-friendly error messages indicating which part is
invalid
- Uses DRY approach with composed regex patterns
### Migration 008: Data Cleanup
- Removes servers with invalid names or empty versions (5 servers
affected in production)
- Fixes invalid status values by setting them to 'active' (1 server
affected)
- Removes duplicate name+version combinations
- Includes comprehensive safety checks to prevent accidental data loss
- Must run before migration 009 which adds strict constraints
## Testing
- All validator tests pass
- Migration includes safety checks that match production data
- CI verification included
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixes migration 008 failing with "column contains null values" error
- Adds COALESCE protection to handle 126 servers without status fields
- Includes test utilities to reproduce and verify fixes with production
data
## Problem
Migration 008 was failing in production because:
- 126 out of 781 servers have no `status` field in their JSON
- When extracting `rec.value->>'status'`, these became NULL values
- The `ALTER TABLE servers ALTER COLUMN status SET NOT NULL` constraint
then failed
## Solution
1. **Prevent NULLs during migration**: Use COALESCE to provide 'active'
default when status is NULL/empty
2. **Enhanced debugging**: Added RAISE NOTICE statements to log
problematic values
3. **Safety check**: Double-check that throws exception if any NULLs
remain
4. **Test utilities**: Created mirror_data tools to test migrations with
real production data
## Testing
Created and used `scripts/mirror_data/` tools to:
- Fetch all 781 servers from production API
- Load them into a test database
- Verify the migration fix works correctly
### Results:
- **Before fix**: 126 NULL status values created during migration
- **After fix**: 0 NULL status values, all servers have valid status
- Migration completes successfully with NOT NULL constraint
## Test plan
- [x] Tested locally with production data using mirror_data tools
- [x] Verified 0 NULL values after migration (vs 126 before)
- [x] Confirmed NOT NULL constraint adds successfully
- [ ] Deploy to staging and verify migration succeeds
- [ ] Monitor production deployment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
<!-- Provide a brief summary of your changes -->
## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following PR tries to ensure sane defaults for entries that for one
reason or another have NULL values for some of their properties.
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
## Breaking Changes
<!-- Will users need to update their code or configurations? -->
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
---------
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
## Problem
Production deployment is failing with CrashLoopBackOff. Migration 008
fails with:
```
ERROR: column "status" of relation "servers" contains null values (SQLSTATE 23502)
```
## Root Cause
The migration extracts the `status` field from an unexpected location in
the data structure.
### Actual Data Structure (Before Migration)
In the current format, `status` is at the **top level** of the JSON, not
nested in official metadata:
```json
{
"name": "com.example/server",
"version": "1.0.0",
"status": "active", ← STATUS IS HERE (top level)
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "uuid",
"publishedAt": "2025-09-01T00:00:00Z",
"isLatest": true
// NO STATUS FIELD HERE
}
}
}
```
For 120 out of 765 servers, the status field is **missing entirely**:
```json
{
"name": "com.biodnd/agent-ip",
"version": "0.1.2",
// ← NO STATUS FIELD AT ALL
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "uuid",
"publishedAt": "2025-09-23T09:47:07Z",
"isLatest": true
// STILL NO STATUS FIELD HERE
}
}
}
```
### Original Migration Logic
```sql
IF official_meta IS NOT NULL THEN
UPDATE servers
SET
status = (official_meta->>'status'), -- Extracts NULL for all servers
```
**Result:**
- All 765 servers would have NULL status extracted
- Because `official_meta->>'status'` returns NULL (field doesn't exist
there)
- The migration tries to add `NOT NULL` constraint → fails
### Fixed Migration Logic
```sql
IF official_meta IS NOT NULL THEN
UPDATE servers
SET
status = COALESCE(NULLIF(rec.value->>'status', 'null'), 'active'),
```
**Result:**
- Extracts from top-level JSON where status actually exists
- `NULLIF(rec.value->>'status', 'null')` handles edge case of literal
string "null"
- `COALESCE(..., 'active')` provides fallback when status is missing
- All servers get valid status value
### Impact Breakdown
**645 servers with `"status": "active"`:**
- Before fix: Would extract NULL → migration fails
- After fix: Extracts "active" correctly
**120 servers with missing status field:**
- Before fix: Would extract NULL → migration fails
- After fix: Defaults to 'active' via COALESCE
## Example Servers with Missing Status
(First 9 discovered during investigation)
- com.biodnd/agent-ip
- io.github.jkakar/cookwith-mcp
- com.joelverhagen.mcp/Knapcode.SampleMcpServer
- io.github.Lyellr88/marm-mcp-server
- com.biodnd/agent-press
- io.github.timheuer/sampledotnetmcpserver
- io.github.CodeCraftersLLC/local-voice-mcp
- com.biodnd/agent-fin
- io.github.ruvnet/ruv-swarm
## Testing
Verified against production data (765 total servers):
- ✅ All servers have official metadata
- ✅ None have status in official metadata (expected)
- ✅ 645 have "status": "active" at top level
- ✅ 120 have missing status field (no field present)
- ✅ Fix extracts from correct location for all cases
## Impact
This unblocks production deployment. Once merged and released as v1.2.1,
the migration will:
1. Extract status from the correct location (top-level JSON)
2. Handle missing status fields with sensible defaults
3. Successfully add NOT NULL constraint
4. Allow pods to start normally
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Fixes race conditions in concurrent server publishing that could result
in data integrity violations:
- Multiple versions marked as `isLatest=true` for the same server
- Duplicate (serverId, version) combinations
- Same server name assigned different serverIDs
## Solution
Uses two complementary approaches for correctness:
### 1. PostgreSQL Advisory Locks
- Serializes concurrent publishes per server name using
`pg_advisory_xact_lock`
- Uses FNV-1a hash to convert server name to lock ID
- Transaction-scoped locks auto-release on commit/rollback
- **Required after #465**: The serverId introduction means concurrent
publishes must atomically coordinate whether to reuse an existing
serverId (for additional versions) or generate a new one (for first
version). Without locks, race conditions would assign different
serverIDs to versions of the same server.
### 2. Unique Constraints (Migration 007)
Two new indexes provide data integrity guarantees:
- **idx_unique_server_version**: Ensures (serverId, version) is unique
across all versions
- **idx_unique_latest_version**: Partial index ensuring only one version
per serverId has `isLatest=true`
These constraints serve as defense-in-depth safety nets. Even if lock
logic had a bug, the database would reject integrity violations.
Also drops redundant `idx_servers_version_id` since `version_id` is
already the primary key.
## Why WithPublishLock validation is needed after #465
The `ctx.Err()` check at the start of `WithPublishLock` is critical
because:
1. After #465 introduced serverId, the publish flow must atomically
check existing versions and assign/reuse serverId
2. If the context is cancelled during lock acquisition, we could
deadlock
3. Checking ctx validity before acquiring locks prevents these deadlocks
## Testing
Added `TestPublishConcurrentVersionsNoRace` with 100 concurrent
goroutines publishing different versions of the same server. All succeed
with:
- Exactly one version marked as latest
- All versions sharing the same serverId
- No duplicate (serverId, version) combinations
NB: this only covers the in memory db, not the postgres db. It'd be nice
to write tests for this one as well, but our current setup is a bit
annoying for this. Separately looking into ways to easily run all our
tests with real postgres and ideally scrap memory.go completely.
## Motivation and Context
- Make all entries in current database matching the new schema
## How Has This Been Tested?
- locally
## Breaking Changes
- Yes
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [X] Breaking change (fix or feature that would cause existing
functionality to change)
- [X] Documentation update
## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [X] My code follows the repository's style guidelines
- [X] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [X] I have added or updated documentation as needed
Before migration:
```
{
"name": "io.github.kenjihikmatullah/productboard-mcp",
"description": "Integrate the Productboard API into agentic workflows via MCP",
"status": "active",
"repository": {
"url": "https://github.com/kenjihikmatullah/productboard-mcp",
"source": "github",
"id": "939104710"
},
"version": "0.0.1-seed",
"packages": [
{
"registry_type": "npm",
"identifier": "productboard-mcp",
"version": "1.0.1",
"transport": {
"type": "stdio"
}
}
],
"_meta": {
"io.modelcontextprotocol.registry/official": {
"id": "00613acb-73e2-4f93-8b96-296df17316c8",
"published_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"is_latest": true
}
}
}
```
After migration:
```
{
"name": "io.github.kenjihikmatullah/productboard-mcp",
"description": "Integrate the Productboard API into agentic workflows via MCP",
"status": "active",
"repository": {
"url": "https://github.com/kenjihikmatullah/productboard-mcp",
"source": "github",
"id": "939104710"
},
"version": "0.0.1-seed",
"packages": [
{
"registryType": "npm",
"identifier": "productboard-mcp",
"version": "1.0.1",
"transport": {
"type": "stdio"
},
}
],
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "83cfe55c-55c5-4e7e-b9c0-6d7d369f52ed",
"versionId": "00613acb-73e2-4f93-8b96-296df17316c8",
"publishedAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z",
"isLatest": true
}
}
}
Wielded Claude Code to do this; across many iterations and have reviewed
all the code + largely rewritten the PR commentary here.
This PR implements the server ID consistency changes from
[#396](https://github.com/modelcontextprotocol/registry/issues/396). Our
goal is to:
- Rename `id` to `versionId` to make it clear that it is the ID for a
specific version, not a server
- Introduce `serverId` as a net new concept. This will be 1:1 with
`name`. While not strictly necessary (we could just use name as a PK),
the convention of having a UUID as an ID on our objects is just a
helpful subjective architecture decision. Plus also leaves open the door
to reconsidering the "should we allow renames" question.
## Key Changes
### 🗄️ Database Schema Changes
- **Renamed**: `id` column → `version_id` (unique identifier for each
server version)
### 📋 Metadata Structure Updates
- **JSON Schema**: Updated
`_meta.io.modelcontextprotocol.registry/official` to include:
- `serverId`: Consistent ID across all versions of a server
- `versionId`: Unique ID for this specific version (renamed from `id`)
### 🔧 API Endpoint Updates
- **New endpoint structure**: Changed from version-ID-based to
server-ID-based URLs:
- `GET /v0/servers/{server_id}` - Get latest version of server
- `GET /v0/servers/{server_id}?version=X.X.X` - Get specific version
- `GET /v0/servers/{server_id}/versions` - List all versions of a server
- `PUT /v0/servers/{server_id}?version=X.X.X` - Edit specific version
- **Consistent URL patterns**: All server endpoints now use `server_id`
as the primary identifier
### Database Migration Strategy
The migration (`005_add_server_id_rename_version_id.sql`):
1. Adds new `version_id` column
2. Generates deterministic UUIDs for servers with the same name
3. Updates JSON metadata to add `serverId` and rename `id` to
`versionId`
4. Copies existing `id` values to `version_id` column
5. Drops old `id` column and makes `version_id` the new primary key
## Testing
Besides of course automated tests passing, I:
1. Set up my local db with the pre-005-migration state, imported all the
production data
2. Ran the migration
3. Spot checked some diffs of the data that resulted --
```json
// before
{
"name": "io.github.ruvnet/ruv-swarm",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"id": "0a9cda7f-2b73-4839-844b-6582f335de42",
"is_latest": false,
"updated_at": "2025-09-10T17:21:34.92137012Z",
"published_at": "2025-09-10T17:15:32.69690433Z"
}
},
"version": "1.0.18",
"repository": {
"url": "",
"source": ""
},
"description": "Neural network swarm orchestration with WebAssembly acceleration and MCP integration"
}
// after
{
"name": "io.github.ruvnet/ruv-swarm",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "e8538952-96af-4a33-9e31-7fbed012521e",
"is_latest": false,
"versionId": "0a9cda7f-2b73-4839-844b-6582f335de42",
"updated_at": "2025-09-10T17:21:34.92137012Z",
"published_at": "2025-09-10T17:15:32.69690433Z"
}
},
"version": "1.0.18",
"repository": {
"url": "",
"source": ""
},
"description": "Neural network swarm orchestration with WebAssembly acceleration and MCP integration"
}
```
Servers that have multiple versions stayed consistent in their
`serverId`:
```json
// First version
{
"name": "app.getdialer/dialer",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "80f08bdb-f3e4-4f19-b072-1676ae49b663",
"is_latest": false,
"versionId": "04f25b17-331d-48d7-b08b-37baece208a7",
"updated_at": "2025-09-09T00:16:49.162620072Z",
"published_at": "2025-09-08T23:47:09.452137971Z"
}
},
"status": "active",
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
"remotes": [
{
"url": "https://getdialer.app/mcp",
"type": "streamable-http"
},
{
"url": "https://getdialer.app/sse",
"type": "sse"
}
],
"version": "1.0.0",
"repository": {
"url": "",
"source": ""
},
"description": "An MCP server that provides your you make outbound phone calls using your own phone number"
}
// Second version
{
"name": "app.getdialer/dialer",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "80f08bdb-f3e4-4f19-b072-1676ae49b663",
"is_latest": true,
"versionId": "faa26cb9-3aad-415e-aece-f715fff5fe5e",
"updated_at": "2025-09-09T00:16:49.107842808Z",
"published_at": "2025-09-09T00:16:49.107842808Z"
}
},
"status": "active",
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
"remotes": [
{
"url": "https://getdialer.app/mcp",
"type": "streamable-http"
},
{
"url": "https://getdialer.app/sse",
"type": "sse"
}
],
"version": "1.0.1",
"repository": {
"url": "",
"source": ""
},
"description": "An MCP server that provides lets you make outbound phone calls using your own phone number"
}
```
And I also tried publishing a test server anew; looked good:
```bash
⏺ Bash(curl -s "http://localhost:8080/v0/servers/102dc4e4-6ca8-40e6-aa11-cd45763d3352" | jq '.')
⎿ {
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
"name": "io.modelcontextprotocol.anonymous/test-claude-migration-server",
"description": "A test server to verify the new migration and API endpoints work correctly",
"status": "active",
"repository": {
"url": "https://github.com/example/test-claude-migration-server",
"source": "github",
"id": "example/test-claude-migration-server"
},
"version": "1.0.0",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "102dc4e4-6ca8-40e6-aa11-cd45763d3352",
"versionId": "48c9cd55-7bc4-4ac2-896e-59e81eb653e4",
"published_at": "2025-09-12T18:33:23.264203-07:00",
"updated_at": "2025-09-12T18:33:23.264203-07:00",
"is_latest": true
}
}
}
⏺ Bash(curl -s "http://localhost:8080/v0/servers/102dc4e4-6ca8-40e6-aa11-cd45763d3352/versions" | jq '.')
⎿ {
"servers": [
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
"name": "io.modelcontextprotocol.anonymous/test-claude-migration-server",
"description": "A test server to verify the new migration and API endpoints work correctly",
"status": "active",
"repository": {
"url": "https://github.com/example/test-claude-migration-server",
"source": "github",
"id": "example/test-claude-migration-server"
},
"version": "1.0.0",
"_meta": {
"io.modelcontextprotocol.registry/official": {
"serverId": "102dc4e4-6ca8-40e6-aa11-cd45763d3352",
"versionId": "48c9cd55-7bc4-4ac2-896e-59e81eb653e4",
"published_at": "2025-09-12T18:33:23.264203-07:00",
"updated_at": "2025-09-12T18:33:23.264203-07:00",
"is_latest": true
}
}
}
],
"metadata": {
"count": 1
}
}
```
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Preeti Dewani <preetidewani1990@gmail.com>
Co-authored-by: adam jones <adamj+git@anthropic.com>
Co-authored-by: Adam Jones <adamj@anthropic.com>
## Summary
Updates the `_meta` field structure to be compliant with the MCP
specification by using proper `prefix/name` format for vendor
extensions.
### Changes
- **Registry metadata**: `io.modelcontextprotocol.registry` →
`io.modelcontextprotocol.registry/official`
- **Publisher metadata**: `publisher` →
`io.modelcontextprotocol.registry/publisher-provided`
- **Database migration**: Updates indexes to use new field paths
- **Updated all code**: Service layer, validators, tests, and seed data
### Format Before
```json
{
"_meta": {
"publisher": { "license": "MIT" },
"io.modelcontextprotocol.registry": { "id": "...", "is_latest": true }
}
}
```
### Format After
```json
{
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": { "license": "MIT" },
"io.modelcontextprotocol.registry/official": { "id": "...", "is_latest": true }
}
}
```
This resolves the naming collision with the planned top-level
`publisher` field and ensures compliance with MCP specification
requirements.
Fixes#356
<!-- Provide a brief summary of your changes -->
This PR serves as a placeholder and brings together all of the changes
related to supporting vendor extensions passed through the API. The idea
is to have a single place to track and review the work as it comes
together (even if it’s still a bit of a work in progress).
**The changes in this PR cover:**
* Updates on the data model
* Updates on both databases (in-memory and postgresql), added a new
table to PSQL for tracking the extensions leaving the server table only
for the clean server.json properties
* Updates the seed.json file (thus the big line change)
* Adds a seed-migrate tool to help migrating an existing seed file to
the new format
* Updates the seed import mechanism to support this
* Updates the existing tests - unit, integration and validation tests
* Adds the publish handler to the OpenAPI spec (it was missing)
* Fixes linting errors
* Updates on the API side on (publish, list, get) to support the new
format of `ServerResponse` -
```
{
"server": {},
"x-io.modelcontextprotocol.registry": {}, /*not available for publish since this gets populated by the registry*/
"x-publisher": {}
}
```
## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Motivated by the discussions in:
https://github.com/modelcontextprotocol/registry/discussions/284
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
Locally and by running all of the tests
## Breaking Changes
<!-- Will users need to update their code or configurations? -->
Yes, it changes both the API and the format of the seed file
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
---
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Co-authored-by: adam jones <domdomegg+git@gmail.com>
Fixes#228
## Motivation and Context
See #19
## How Has This Been Tested?
- Run locally
- Run in docker compose
- Run infra in minikube
- Unit and integration tests all passing :)
## Breaking Changes
Yes: users will not be able to access any data they used to have in
custom mongodb. We are accepting this breaking change, given the
registry is in experimental development status.
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed
## Additional context
I have not implemented connection pooling support for simplicity, and
because cloudnative-pg already provides built-in connection pooling with
pgBouncer so if we want it later we can switch to this endpoint easily.