diff --git a/apps/registry/src/routes/packages.ts b/apps/registry/src/routes/packages.ts index 0d01b90..3b20535 100644 --- a/apps/registry/src/routes/packages.ts +++ b/apps/registry/src/routes/packages.ts @@ -30,8 +30,8 @@ import { triggerSecurityScan } from '../services/scanner.js'; import type { MCPBManifest } from '../types.js'; // Package name validation -const UNSCOPED_REGEX = /^[a-z0-9][a-z0-9-]{0,213}$/; -const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,213}$/; +const UNSCOPED_REGEX = /^[a-z0-9][a-z0-9-]{0,213}$/i; +const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,213}$/i; function parsePackageName(name: string): { scope: string | null; packageName: string; isScoped: boolean } { if (name.startsWith('@')) { diff --git a/apps/registry/src/routes/v1/bundles.ts b/apps/registry/src/routes/v1/bundles.ts index 30aa5b3..5035abe 100644 --- a/apps/registry/src/routes/v1/bundles.ts +++ b/apps/registry/src/routes/v1/bundles.ts @@ -47,7 +47,7 @@ function getPlatformString(os: string, arch: string): string { } // Package name validation (scoped only for v1 API) -const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,213}$/; +const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,213}$/i; function isValidScopedPackageName(name: string): boolean { return SCOPED_REGEX.test(name); @@ -769,7 +769,7 @@ export const bundleRoutes: FastifyPluginAsync = async (fastify) => { // Validate package name if (!isValidScopedPackageName(name)) { throw new BadRequestError( - `Invalid package name: "${name}". Must be scoped (@scope/name) with lowercase alphanumeric characters and hyphens.` + `Invalid package name: "${name}". Must be scoped (@scope/name) with alphanumeric characters and hyphens.` ); } diff --git a/apps/registry/src/routes/v1/skills.ts b/apps/registry/src/routes/v1/skills.ts index d6aec1e..0a7dd94 100644 --- a/apps/registry/src/routes/v1/skills.ts +++ b/apps/registry/src/routes/v1/skills.ts @@ -34,7 +34,7 @@ interface GitHubReleaseAsset { } // Scoped name validation -const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]*$/; +const SCOPED_REGEX = /^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]*$/i; function isValidScopedName(name: string): boolean { return SCOPED_REGEX.test(name); @@ -505,7 +505,7 @@ export const skillRoutes: FastifyPluginAsync = async (fastify) => { // Validate name if (!isValidScopedName(name)) { throw new BadRequestError( - `Invalid skill name: "${name}". Must be scoped (@scope/name) with lowercase alphanumeric characters and hyphens.` + `Invalid skill name: "${name}". Must be scoped (@scope/name) with alphanumeric characters and hyphens.` ); } diff --git a/apps/registry/tests/bundles.test.ts b/apps/registry/tests/bundles.test.ts index a2b02c9..90e0280 100644 --- a/apps/registry/tests/bundles.test.ts +++ b/apps/registry/tests/bundles.test.ts @@ -503,6 +503,46 @@ describe('Bundle Routes', () => { expect(res.statusCode).toBe(400); }); + it('accepts package name with mixed-case scope', async () => { + (verifyGitHubOIDC as Mock).mockResolvedValue({ + ...validOIDCClaims, + repository: 'Test-Org/mcp-server', + repository_owner: 'Test-Org', + }); + + const res = await app.inject({ + method: 'POST', + url: '/announce', + headers: { authorization: 'Bearer valid-token' }, + payload: { + ...validPayload, + name: '@Test-Org/mcp-server', + }, + }); + + // The request should pass name validation (not get rejected for invalid name) + const body = JSON.parse(res.payload); + if (res.statusCode === 400) { + expect(body.error.message).not.toContain('Invalid package name'); + } + }); + + it('rejects package name with invalid characters', async () => { + (verifyGitHubOIDC as Mock).mockResolvedValue(validOIDCClaims); + + const res = await app.inject({ + method: 'POST', + url: '/announce', + headers: { authorization: 'Bearer valid-token' }, + payload: { + ...validPayload, + name: '@test-org/mcp server!', + }, + }); + + expect(res.statusCode).toBe(400); + }); + it('rejects manifest without server type', async () => { (verifyGitHubOIDC as Mock).mockResolvedValue(validOIDCClaims);