diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b679f4..19d8898 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -68,3 +68,5 @@ jobs: cache: npm - run: npm ci - run: npm run test:mcp:packed + - run: npm run build:mcpb + - run: npm run proof:mcpb diff --git a/.gitignore b/.gitignore index 9ba48f9..338118e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ packages/*/docs *.log .DS_Store .vitest-cache +artifacts/ # Local agent tooling .claude/ diff --git a/README.md b/README.md index 2e2c1d2..27e7fa9 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,19 @@ Add to your MCP client config (Claude Desktop, Claude Code, Cursor, …): The server exposes 11 tools for DFS validation and settlement, odds and bet-history math, and game scoring. It performs deterministic computation only; it does not fetch operator accounts, live odds, or box scores. See the [MCP install, client configuration, tool catalog, and error contracts](packages/mcp/README.md). +A downloadable MCPB built from the exact `@buzzr/mcp@5.1.0` npm artifact is +published as the [Buzzr Sports Engine on +Smithery](https://smithery.ai/servers/sarveshsea/buzzr-sports-engine). Smithery +distributes it as a local stdio MCPB, so the tools still run on your machine. It +is not a hosted HTTP service. For a Codex install through Smithery: + +```sh +npx -y smithery@1.2.0 mcp add sarveshsea/buzzr-sports-engine --client codex +``` + +Use the direct version-pinned npm configuration above when you also need to pin +the launcher rather than accept the Smithery-generated runner configuration. + ## Verified Buzzr app integration The Buzzr mobile app’s `release/ios-2.0.0` branch vendors `@buzzr/bets-core`, `@buzzr/dfs-engine`, and `@buzzr/entertainment-engine` as local 5.0.0 tarballs and imports all three. That verified snapshot is not automatically upgraded to the public 5.1.0 toolkit; an app update remains a separate, deliberate release task. @@ -209,6 +222,7 @@ For settlement correctness or security-sensitive issues, follow [SECURITY.md](SE - [All-package API index](docs/api-reference.md) — supported roots for all ten packages - [Buzzr Sports Engine skill](skills/buzzr-sports-engine/SKILL.md) — Codex workflow and safety contract - [MCP configuration](packages/mcp/README.md) — install and client setup +- [Smithery distribution](https://smithery.ai/servers/sarveshsea/buzzr-sports-engine) — local stdio MCPB for all 11 tools ## License diff --git a/docs/security-and-privacy.md b/docs/security-and-privacy.md index 835b6f9..e01523f 100644 --- a/docs/security-and-privacy.md +++ b/docs/security-and-privacy.md @@ -8,7 +8,7 @@ This document defines the public package and local MCP security boundary. It is - Integrity of the policy/table version and source metadata used for a decision. - Availability of the local MCP process. - Confidentiality of entries, bet history, identifiers, provider rows, and host environment data. -- Integrity and provenance of published npm and GitHub release artifacts. +- Integrity and provenance of published npm, GitHub, and Smithery release artifacts. ## Trust boundaries @@ -18,6 +18,7 @@ This document defines the public package and local MCP security boundary. It is | Data loader → `StatProvider` | Provider contract and runtime validator | Network/vendor response mapped by the caller | | MCP client → stdio server | Packed server artifact and MCP SDK | JSON-RPC frames, strings, arrays, IDs, odds, entries, game/history data | | npm/GitHub → local install | Reviewed release workflow, digest, `gitHead`, provenance | Registry/network delivery until verified | +| Smithery → local install | Exact MCPB digest and runtime-discovered server card | Registry/network delivery until downloaded and verified | | Engine result → user decision | Deterministic output for the supplied profile and data | Operator rules, displayed terms, and real-world rulings outside the engine | The core engines trust neither caller input nor operator-named compatibility data as proof of a real operator outcome. Validate at system boundaries and retain the result's verification and provenance fields. @@ -58,6 +59,13 @@ An MCP host can expose other capabilities in the same session. Treat text echoed Release proof must bind an npm artifact to the reviewed exact version, SHA-512 integrity, registry tarball origin, Git `gitHead`, and provenance attestation. CI uses packed-artifact real-client tests before publication, and the published proof runs in isolated temporary npm/home state with lifecycle scripts disabled. +The Smithery MCPB repackages that exact npm artifact without adding secrets or +a network transport. Publication discovers the full tool schemas from the real +server and waits for a successful registry release. For the 5.1.0 Smithery +release, the registry copy was then downloaded, its SHA-256 digest was compared +with the uploaded bundle, and the real-client proof was replayed against the +downloaded bytes. + Run `npm run audit:high` and `npm run verify` before release. An audit result is one signal, not proof that dependencies or the registry are uncompromised. ### Operator and financial risk @@ -69,6 +77,7 @@ Odds, expected-value, Kelly, and settlement outputs are calculations from suppli ## Deployment guidance - Keep MCP on local stdio unless an embedding application adds its own authentication, authorization, rate limiting, TLS, request isolation, and audit policy. +- Treat the Smithery listing as a local stdio distribution, not as a hosted service or a new trust boundary for tool calls. - Pin `@buzzr/mcp@5.1.0` when repeatability matters. - Run the server as a non-privileged user with the smallest environment and filesystem access the host permits. - Do not place secrets in MCP configuration because Buzzr needs none. diff --git a/llms.txt b/llms.txt index 072ca28..f1becd6 100644 --- a/llms.txt +++ b/llms.txt @@ -31,6 +31,7 @@ Live app: [Buzzr Sports on the App Store](https://apps.apple.com/us/app/buzzr-sp - Repository skill: [skills/buzzr-sports-engine/SKILL.md](skills/buzzr-sports-engine/SKILL.md) - Install: `npx skills add https://github.com/Buzzr-app/dfs-engine --skill buzzr-sports-engine` - MCP install, client configuration, tools, and errors: [packages/mcp/README.md](packages/mcp/README.md) +- Smithery distribution: [sarveshsea/buzzr-sports-engine](https://smithery.ai/servers/sarveshsea/buzzr-sports-engine), a local stdio MCPB containing the exact @buzzr/mcp@5.1.0 artifact and all 11 tools. It is not a hosted HTTP service. ## Docs diff --git a/package-lock.json b/package-lock.json index 18a525b..af55644 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "ajv": "8.20.0", "ajv-formats": "3.0.1", "eslint": "^10.4.0", + "fflate": "0.8.2", "prettier": "^3.8.3", "skills": "1.5.17", "tsup": "^8.5.1", @@ -3542,6 +3543,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", diff --git a/package.json b/package.json index 24432bf..577dadf 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "size:check": "node scripts/check-package-size.mjs", "audit:high": "npm audit --audit-level=high", "check:workflows": "node scripts/check-release-workflows.mjs", - "test:release-supply-chain": "node --test tests/release-supply-chain.test.mjs tests/npm-pack-output.test.mjs tests/npm-view-output.test.mjs", + "test:release-supply-chain": "node --test tests/release-supply-chain.test.mjs tests/npm-pack-output.test.mjs tests/npm-view-output.test.mjs tests/smithery-bundle.test.mjs", "check:mcp-registry": "node scripts/check-mcp-registry-metadata.mjs", "check:api-docs": "node scripts/check-api-docs.mjs", "check:docs": "node scripts/check-public-docs.mjs", @@ -36,11 +36,14 @@ "check:skill": "node scripts/validate-buzzr-skill.mjs", "capture:adoption": "node scripts/capture-adoption-metrics.mjs", "test:adoption-capture": "node --test scripts/tests/capture-adoption-metrics.test.mjs", - "verify": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run build && npm run test:mcp:packed && npm run test:packages:packed && npm run check:mcp:client-skill && npm run check:mcp:examples && npm run check:skill && npm run docs && npm run check:docs && npm run check:links && npm run smoke:exports && npm run size:check && npm run pack:smoke && npm run check:workflows && npm run test:release-supply-chain && npm run check:mcp-registry && npm run audit:high", + "verify": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run build && npm run test:mcp:packed && npm run build:mcpb && npm run proof:mcpb && npm run test:packages:packed && npm run check:mcp:client-skill && npm run check:mcp:examples && npm run check:skill && npm run docs && npm run check:docs && npm run check:links && npm run smoke:exports && npm run size:check && npm run pack:smoke && npm run check:workflows && npm run test:release-supply-chain && npm run check:mcp-registry && npm run audit:high", "pack:smoke": "npm pack --workspaces --dry-run", "test:mcp:packed": "npm run build --workspace @buzzr/bets-core --workspace @buzzr/dfs-engine --workspace @buzzr/entertainment-engine --workspace @buzzr/mcp && node scripts/test-mcp-packed.mjs", "test:packages:packed": "node scripts/test-release-artifacts.mjs", - "proof:mcp:published": "node scripts/prove-mcp-published.mjs" + "proof:mcp:published": "node scripts/prove-mcp-published.mjs", + "build:mcpb": "node scripts/build-smithery-bundle.mjs", + "proof:mcpb": "node scripts/prove-smithery-bundle.mjs", + "publish:smithery": "node scripts/publish-smithery-bundle.mjs" }, "devDependencies": { "@changesets/cli": "^2.31.0", @@ -49,6 +52,7 @@ "ajv": "8.20.0", "ajv-formats": "3.0.1", "eslint": "^10.4.0", + "fflate": "0.8.2", "prettier": "^3.8.3", "skills": "1.5.17", "tsup": "^8.5.1", diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 2ff115e..27b6758 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -28,6 +28,22 @@ The server speaks MCP over stdio: JSON-RPC on stdin/stdout, logs on stderr. It exposes 11 tools with bounded inputs and outputs, and does not fetch live odds, box scores, operator accounts, or private user data. +### Smithery + +The [Buzzr Sports Engine listing on +Smithery](https://smithery.ai/servers/sarveshsea/buzzr-sports-engine) packages +the exact published `@buzzr/mcp@5.1.0` artifact as a local stdio MCPB. It is a +distribution and installation surface, not a hosted HTTP service. To add it to +Codex through Smithery: + +```sh +npx -y smithery@1.2.0 mcp add sarveshsea/buzzr-sports-engine --client codex +``` + +Smithery writes the corresponding MCP configuration for the selected client. +Use the direct npm command above when you need both the server package and its +launcher configuration pinned explicitly. + ### Claude Desktop Add this to `claude_desktop_config.json` from **Settings → Developer → Edit diff --git a/scripts/build-smithery-bundle.mjs b/scripts/build-smithery-bundle.mjs new file mode 100644 index 0000000..42fbbdc --- /dev/null +++ b/scripts/build-smithery-bundle.mjs @@ -0,0 +1,182 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { basename, dirname, join, resolve } from 'node:path'; +import { promisify } from 'node:util'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { parseNpmViewValue } from './lib/npm-view-output.mjs'; +import { runMcpb } from './lib/mcpb-cli.mjs'; +import { + createSmitheryManifest, + SMITHERY_QUALIFIED_NAME, + smitheryBundleFilename, +} from './lib/smithery-bundle.mjs'; + +const execFileAsync = promisify(execFile); +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const smitheryDirectory = join(root, 'smithery'); +const outputDirectory = join(root, 'artifacts', 'smithery'); +const maximumBundleBytes = 25 * 1024 * 1024; + +function execNpm(args, options) { + if (process.env.npm_execpath) { + return execFileAsync(process.execPath, [process.env.npm_execpath, ...args], options); + } + return execFileAsync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, options); +} + +function sha256(buffer) { + return createHash('sha256').update(buffer).digest('hex'); +} + +const temporaryRoot = await mkdtemp(join(tmpdir(), 'buzzr-smithery-build-')); +const bundleDirectory = join(temporaryRoot, 'bundle'); +const serverDirectory = join(bundleDirectory, 'server'); +const emptyUserConfig = join(temporaryRoot, 'empty-user.npmrc'); +const emptyGlobalConfig = join(temporaryRoot, 'empty-global.npmrc'); + +try { + const [runtimeManifest, runtimeLock, source] = await Promise.all( + ['package.json', 'package-lock.json', 'source.json'].map(async (filename) => + JSON.parse(await readFile(join(smitheryDirectory, filename), 'utf8')), + ), + ); + assert.equal(source.schemaVersion, 1); + assert.equal(source.qualifiedName, SMITHERY_QUALIFIED_NAME); + assert.equal(runtimeManifest.version, source.version); + assert.equal(runtimeManifest.dependencies[source.package], source.version); + + const lockedMcp = runtimeLock.packages?.[`node_modules/${source.package}`]; + assert(lockedMcp, `${source.package} is missing from the Smithery runtime lockfile`); + assert.equal(lockedMcp.version, source.version); + assert.equal(lockedMcp.integrity, source.integrity); + assert.equal(lockedMcp.resolved, source.tarball); + + await mkdir(serverDirectory, { recursive: true }); + await mkdir(outputDirectory, { recursive: true }); + await Promise.all([ + cp(join(smitheryDirectory, 'package.json'), join(serverDirectory, 'package.json')), + cp(join(smitheryDirectory, 'package-lock.json'), join(serverDirectory, 'package-lock.json')), + cp(join(smitheryDirectory, 'source.json'), join(bundleDirectory, 'source.json')), + cp(join(root, 'packages', 'mcp', 'LICENSE'), join(bundleDirectory, 'LICENSE')), + cp(join(root, 'packages', 'mcp', 'README.md'), join(bundleDirectory, 'README.md')), + writeFile(emptyUserConfig, '', { mode: 0o600 }), + writeFile(emptyGlobalConfig, '', { mode: 0o600 }), + ]); + + const registryEnvironment = { + ...process.env, + NO_COLOR: '1', + NPM_CONFIG_AUDIT: 'false', + NPM_CONFIG_FUND: 'false', + NPM_CONFIG_GLOBALCONFIG: emptyGlobalConfig, + NPM_CONFIG_REGISTRY: source.registry, + NPM_CONFIG_UPDATE_NOTIFIER: 'false', + NPM_CONFIG_USERCONFIG: emptyUserConfig, + }; + + const view = await execNpm( + [ + 'view', + `${source.package}@${source.version}`, + 'version', + 'gitHead', + 'dist.integrity', + 'dist.tarball', + '--json', + ], + { cwd: root, env: registryEnvironment, maxBuffer: 2 * 1024 * 1024 }, + ); + const live = parseNpmViewValue(view.stdout, `${source.package}@${source.version}`); + assert.equal(live.version, source.version, 'Published MCP version changed'); + assert.equal(live.gitHead, source.gitHead, 'Published MCP gitHead changed'); + assert.equal(live['dist.integrity'], source.integrity, 'Published MCP integrity changed'); + assert.equal(live['dist.tarball'], source.tarball, 'Published MCP tarball changed'); + + await execNpm(['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { + cwd: serverDirectory, + env: { + ...registryEnvironment, + npm_config_cache: join(temporaryRoot, 'npm-cache'), + }, + maxBuffer: 20 * 1024 * 1024, + }); + await rm(join(serverDirectory, 'node_modules', '.bin'), { recursive: true, force: true }); + + const installedPackagePath = join(serverDirectory, 'node_modules', ...source.package.split('/')); + const installedManifest = JSON.parse( + await readFile(join(installedPackagePath, 'package.json'), 'utf8'), + ); + assert.equal(installedManifest.name, source.package); + assert.equal(installedManifest.version, source.version); + assert.equal(installedManifest.gitHead, source.gitHead); + + const installedModule = await import( + `${pathToFileURL(join(installedPackagePath, 'dist', 'index.js')).href}?bundle=${Date.now()}` + ); + const tools = installedModule.allTools.map(({ name, description }) => ({ name, description })); + assert.equal(tools.length, 11, 'Smithery bundle must expose exactly 11 Buzzr tools'); + + const manifest = createSmitheryManifest({ version: source.version, tools }); + await Promise.all([ + writeFile(join(bundleDirectory, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`), + writeFile( + join(serverDirectory, 'index.js'), + "import './node_modules/@buzzr/mcp/dist/cli.js';\n", + ), + ]); + + await runMcpb(['validate', join(bundleDirectory, 'manifest.json')], { + cwd: root, + env: { + NPM_CONFIG_CACHE: join(temporaryRoot, 'mcpb-cache'), + NPM_CONFIG_GLOBALCONFIG: emptyGlobalConfig, + NPM_CONFIG_REGISTRY: source.registry, + NPM_CONFIG_USERCONFIG: emptyUserConfig, + }, + }); + + const artifactPath = join(outputDirectory, smitheryBundleFilename(source.version)); + const metadataPath = `${artifactPath}.json`; + await Promise.all([rm(artifactPath, { force: true }), rm(metadataPath, { force: true })]); + await runMcpb(['pack', bundleDirectory, artifactPath], { + cwd: root, + env: { + NPM_CONFIG_CACHE: join(temporaryRoot, 'mcpb-cache'), + NPM_CONFIG_GLOBALCONFIG: emptyGlobalConfig, + NPM_CONFIG_REGISTRY: source.registry, + NPM_CONFIG_USERCONFIG: emptyUserConfig, + }, + }); + + const artifact = await readFile(artifactPath); + const artifactStat = await stat(artifactPath); + assert.equal(artifactStat.isFile(), true); + assert( + artifactStat.size <= maximumBundleBytes, + `Smithery bundle exceeds ${maximumBundleBytes} bytes`, + ); + + const metadata = { + schemaVersion: 1, + qualifiedName: source.qualifiedName, + artifact: basename(artifactPath), + version: source.version, + sha256: sha256(artifact), + size: artifactStat.size, + toolCount: tools.length, + npm: { + package: source.package, + version: source.version, + gitHead: source.gitHead, + integrity: source.integrity, + }, + }; + await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`); + process.stdout.write(`${JSON.stringify({ artifactPath, metadataPath, ...metadata })}\n`); +} finally { + await rm(temporaryRoot, { recursive: true, force: true }); +} diff --git a/scripts/check-mcp-client-skill-contracts.mjs b/scripts/check-mcp-client-skill-contracts.mjs index bfc4c0b..23b04ad 100644 --- a/scripts/check-mcp-client-skill-contracts.mjs +++ b/scripts/check-mcp-client-skill-contracts.mjs @@ -18,6 +18,11 @@ function requirePattern(text, pattern, message) { } const clientContracts = [ + [ + 'Smithery', + /### Smithery/, + /npx -y smithery@1\.2\.0 mcp add sarveshsea\/buzzr-sports-engine --client codex/, + ], ['Claude Desktop', /### Claude Desktop/, /claude_desktop_config\.json/], [ 'Claude Code', @@ -34,6 +39,13 @@ for (const [client, heading, setup] of clientContracts) { requirePattern(readme, setup, `@buzzr/mcp README must provide copy-paste ${client} setup`); } +requirePattern(readme, /local stdio MCPB/i, 'README must identify Smithery as local stdio'); +requirePattern( + readme, + /not a hosted HTTP service/i, + 'README must not describe the Smithery distribution as a hosted MCP service', +); + for (const discoveryTerm of [ /`initialize`/, /`serverInfo\.name`/, diff --git a/scripts/check-public-docs.mjs b/scripts/check-public-docs.mjs index d8ffd4e..cbe892b 100644 --- a/scripts/check-public-docs.mjs +++ b/scripts/check-public-docs.mjs @@ -233,9 +233,27 @@ for (const spec of packageReadmes) { for (const key of ['root', 'llms', 'mcp']) { requirePattern(key, /\b11 tools\b/i, 'state the current MCP tool count'); + requireText( + key, + 'https://smithery.ai/servers/sarveshsea/buzzr-sports-engine', + 'link the live Smithery distribution', + ); } requirePattern('agents', /\b11 tools\b/i, 'state the current MCP tool count'); +for (const key of ['root', 'llms', 'mcp']) { + requirePattern(key, /local stdio MCPB/i, 'describe the Smithery transport accurately'); + requirePattern(key, /not a hosted HTTP service/i, 'reject a hosted Smithery transport claim'); +} + +for (const key of ['root', 'mcp']) { + requireText( + key, + 'npx -y smithery@1.2.0 mcp add sarveshsea/buzzr-sports-engine --client codex', + 'provide the verified Smithery Codex install command', + ); +} + const toolNames = [ 'grade_dfs_entry', 'grade_dfs_entries', diff --git a/scripts/lib/mcpb-cli.mjs b/scripts/lib/mcpb-cli.mjs new file mode 100644 index 0000000..f1bb0e4 --- /dev/null +++ b/scripts/lib/mcpb-cli.mjs @@ -0,0 +1,182 @@ +import assert from 'node:assert/strict'; +import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; +import { dirname, join, posix, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +import { unzipSync, zipSync } from 'fflate'; + +const schemaPath = fileURLToPath( + new URL('../../smithery/mcpb-manifest-v0.4.schema.json', import.meta.url), +); +const archiveEpoch = new Date(1980, 0, 2, 0, 0, 0); +const maximumArchiveEntries = 20_000; +const maximumExtractedBytes = 100 * 1024 * 1024; + +function safeArchiveName(name) { + assert(name.length > 0, 'MCPB contains an empty archive path'); + assert(!name.includes('\0'), 'MCPB archive path contains a null byte'); + assert(!name.includes('\\'), `MCPB archive path uses a backslash: ${name}`); + assert(!posix.isAbsolute(name), `MCPB archive path is absolute: ${name}`); + assert(!/^[A-Za-z]:/.test(name), `MCPB archive path uses a drive prefix: ${name}`); + assert.equal(posix.normalize(name), name, `MCPB archive path is not normalized: ${name}`); + assert(!name.startsWith('../') && !name.includes('/../'), `MCPB archive path escapes: ${name}`); + return name; +} + +function endOfCentralDirectory(archive) { + const minimumOffset = Math.max(0, archive.length - 65_557); + for (let offset = archive.length - 22; offset >= minimumOffset; offset -= 1) { + if (archive.readUInt32LE(offset) === 0x06054b50) return offset; + } + throw new Error('MCPB has no ZIP end-of-central-directory record'); +} + +export function inspectMcpbArchive(bytes) { + const archive = Buffer.from(bytes); + assert(archive.length >= 22, 'MCPB is too small to be a ZIP archive'); + const endOffset = endOfCentralDirectory(archive); + assert.equal(archive.readUInt16LE(endOffset + 4), 0, 'Multi-disk MCPB archives are unsupported'); + assert.equal(archive.readUInt16LE(endOffset + 6), 0, 'Multi-disk MCPB archives are unsupported'); + const diskEntries = archive.readUInt16LE(endOffset + 8); + const totalEntries = archive.readUInt16LE(endOffset + 10); + assert.equal(diskEntries, totalEntries, 'Multi-disk MCPB archives are unsupported'); + assert(totalEntries < 0xffff, 'ZIP64 MCPB archives are unsupported'); + assert(totalEntries <= maximumArchiveEntries, 'MCPB contains too many archive entries'); + const centralSize = archive.readUInt32LE(endOffset + 12); + const centralOffset = archive.readUInt32LE(endOffset + 16); + assert( + centralOffset + centralSize <= endOffset, + 'MCPB central directory is outside the archive', + ); + + let cursor = centralOffset; + let totalBytes = 0; + const entries = []; + const names = new Set(); + for (let index = 0; index < totalEntries; index += 1) { + assert.equal( + archive.readUInt32LE(cursor), + 0x02014b50, + 'MCPB central directory entry is invalid', + ); + const flags = archive.readUInt16LE(cursor + 8); + const compression = archive.readUInt16LE(cursor + 10); + const compressedSize = archive.readUInt32LE(cursor + 20); + const uncompressedSize = archive.readUInt32LE(cursor + 24); + const nameLength = archive.readUInt16LE(cursor + 28); + const extraLength = archive.readUInt16LE(cursor + 30); + const commentLength = archive.readUInt16LE(cursor + 32); + const externalAttributes = archive.readUInt32LE(cursor + 38); + const localOffset = archive.readUInt32LE(cursor + 42); + const next = cursor + 46 + nameLength + extraLength + commentLength; + assert(next <= centralOffset + centralSize, 'MCPB central directory entry is truncated'); + assert.equal(flags & 1, 0, 'Encrypted MCPB entries are unsupported'); + assert( + compression === 0 || compression === 8, + `Unsupported MCPB compression method: ${compression}`, + ); + assert(localOffset + 30 <= centralOffset, 'MCPB local file header is outside the archive'); + assert.equal(archive.readUInt32LE(localOffset), 0x04034b50, 'MCPB local file header is invalid'); + + const name = safeArchiveName( + archive.subarray(cursor + 46, cursor + 46 + nameLength).toString('utf8'), + ); + assert(!names.has(name), `MCPB contains a duplicate archive path: ${name}`); + names.add(name); + const unixFileType = Math.floor(externalAttributes / 65_536) & 0o170000; + assert.notEqual(unixFileType, 0o120000, `MCPB contains a symbolic link: ${name}`); + totalBytes += uncompressedSize; + assert(totalBytes <= maximumExtractedBytes, 'MCPB declared content is too large'); + entries.push({ name, compressedSize, uncompressedSize }); + cursor = next; + } + assert.equal(cursor, centralOffset + centralSize, 'MCPB central directory size is inconsistent'); + return Object.freeze({ entries: Object.freeze(entries), totalBytes }); +} + +async function collectFiles(directory) { + const files = {}; + let totalBytes = 0; + async function visit(current, prefix = '') { + const entries = await readdir(current, { withFileTypes: true }); + entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)); + for (const entry of entries) { + const path = join(current, entry.name); + const pathStat = await lstat(path); + assert.equal(pathStat.isSymbolicLink(), false, `Cannot pack symbolic link: ${path}`); + const name = safeArchiveName(prefix ? `${prefix}/${entry.name}` : entry.name); + if (pathStat.isDirectory()) { + await visit(path, name); + } else { + assert.equal(pathStat.isFile(), true, `Cannot pack non-file entry: ${path}`); + totalBytes += pathStat.size; + assert(totalBytes <= maximumExtractedBytes, 'MCPB source content is too large'); + files[name] = new Uint8Array(await readFile(path)); + } + } + } + await visit(directory); + assert(Object.keys(files).length > 0, 'Cannot pack an empty MCPB directory'); + assert(Object.keys(files).length <= maximumArchiveEntries, 'MCPB source has too many files'); + return files; +} + +async function validateManifest(path) { + const [manifest, schema] = await Promise.all( + [path, schemaPath].map(async (filename) => JSON.parse(await readFile(filename, 'utf8'))), + ); + const ajv = new Ajv({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(schema); + assert.equal( + validate(manifest), + true, + `MCPB manifest is invalid: ${ajv.errorsText(validate.errors)}`, + ); +} + +async function pack(directory, artifactPath) { + const files = await collectFiles(resolve(directory)); + const archive = zipSync(files, { level: 9, mtime: archiveEpoch }); + inspectMcpbArchive(archive); + await mkdir(dirname(resolve(artifactPath)), { recursive: true }); + await writeFile(resolve(artifactPath), archive, { mode: 0o644 }); +} + +async function unpack(artifactPath, outputDirectory) { + const archive = await readFile(resolve(artifactPath)); + const inspected = inspectMcpbArchive(archive); + const extracted = unzipSync(archive); + assert.deepEqual(Object.keys(extracted).sort(), inspected.entries.map(({ name }) => name).sort()); + const outputRoot = resolve(outputDirectory); + let totalBytes = 0; + for (const [name, data] of Object.entries(extracted)) { + const path = resolve(outputRoot, name); + assert(path.startsWith(`${outputRoot}${sep}`), `MCPB archive path escapes output: ${name}`); + if (name.endsWith('/')) { + await mkdir(path, { recursive: true }); + continue; + } + totalBytes += data.byteLength; + assert(totalBytes <= maximumExtractedBytes, 'MCPB extracted content is too large'); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, data, { flag: 'wx', mode: 0o600 }); + } +} + +export async function runMcpb(args) { + assert(Array.isArray(args) && args.every((argument) => typeof argument === 'string')); + const [command, ...operands] = args; + if (command === 'validate' && operands.length === 1) { + await validateManifest(resolve(operands[0])); + } else if (command === 'pack' && operands.length === 2) { + await pack(operands[0], operands[1]); + } else if (command === 'unpack' && operands.length === 2) { + await unpack(operands[0], operands[1]); + } else { + throw new TypeError(`Unsupported MCPB command or arguments: ${command ?? ''}`); + } + return { stdout: '', stderr: '' }; +} diff --git a/scripts/lib/smithery-bundle.mjs b/scripts/lib/smithery-bundle.mjs new file mode 100644 index 0000000..8329c97 --- /dev/null +++ b/scripts/lib/smithery-bundle.mjs @@ -0,0 +1,99 @@ +const MCPB_SCHEMA = + 'https://raw.githubusercontent.com/modelcontextprotocol/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json'; +const SEMANTIC_VERSION = + /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/; +const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/; + +export const SMITHERY_QUALIFIED_NAME = 'sarveshsea/buzzr-sports-engine'; +export const SMITHERY_BUNDLE_NAME = 'buzzr-sports-engine'; + +function requireSemanticVersion(version) { + if (typeof version !== 'string' || !SEMANTIC_VERSION.test(version)) { + throw new TypeError('Smithery bundle version must be an exact semantic version.'); + } + return version; +} + +function copyTools(tools) { + if (!Array.isArray(tools) || tools.length === 0) { + throw new TypeError('Smithery bundle tools must be a non-empty array.'); + } + + const copied = tools.map((tool) => { + if (!tool || typeof tool !== 'object' || !TOOL_NAME.test(tool.name)) { + throw new TypeError('Every Smithery tool name must be a safe MCP identifier.'); + } + if ( + typeof tool.description !== 'string' || + tool.description.length === 0 || + tool.description.length > 2_000 + ) { + throw new TypeError('Every Smithery tool description must contain 1 to 2,000 characters.'); + } + return { name: tool.name, description: tool.description }; + }); + + if (new Set(copied.map(({ name }) => name)).size !== copied.length) { + throw new TypeError('Smithery tool names must be unique.'); + } + return copied; +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value; + } + for (const nested of Object.values(value)) { + deepFreeze(nested); + } + return Object.freeze(value); +} + +export function createSmitheryManifest({ version, tools }) { + const exactVersion = requireSemanticVersion(version); + const copiedTools = copyTools(tools); + + return deepFreeze({ + $schema: MCPB_SCHEMA, + manifest_version: '0.4', + name: SMITHERY_BUNDLE_NAME, + display_name: 'Buzzr Sports Engine', + version: exactVersion, + description: 'Local sports math, DFS settlement, bet analytics, and game-entertainment tools.', + long_description: + 'Deterministic, auditable sports computation through 11 MCP tools. Buzzr does not fetch live lines, access operator accounts, or require credentials.', + author: { + name: 'Sarvesh Chidambaram', + url: 'https://github.com/Buzzr-app', + }, + repository: { + type: 'git', + url: 'https://github.com/Buzzr-app/dfs-engine', + }, + homepage: 'https://buzzr-app.github.io/dfs-engine/', + documentation: 'https://github.com/Buzzr-app/dfs-engine/tree/main/packages/mcp', + support: 'https://github.com/Buzzr-app/dfs-engine/issues', + license: 'MIT', + keywords: ['sports', 'dfs', 'odds', 'betting-math', 'model-context-protocol', 'buzzr'], + server: { + type: 'node', + entry_point: 'server/index.js', + mcp_config: { + command: 'node', + args: ['${__dirname}/server/index.js'], + env: {}, + }, + }, + tools: copiedTools, + tools_generated: false, + prompts_generated: false, + compatibility: { + platforms: ['darwin', 'win32', 'linux'], + runtimes: { node: '>=22' }, + }, + }); +} + +export function smitheryBundleFilename(version) { + return `${SMITHERY_BUNDLE_NAME}-${requireSemanticVersion(version)}.mcpb`; +} diff --git a/scripts/lib/smithery-release.mjs b/scripts/lib/smithery-release.mjs new file mode 100644 index 0000000..1bd6021 --- /dev/null +++ b/scripts/lib/smithery-release.mjs @@ -0,0 +1,55 @@ +const SEMANTIC_VERSION = + /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/; +const SERVER_NAME = /^[a-z][a-z0-9_-]{2,63}$/; +const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/; + +function isObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value; + } + for (const nested of Object.values(value)) { + deepFreeze(nested); + } + return Object.freeze(value); +} + +export function createSmitheryReleasePayload({ serverInfo, tools }) { + if (!isObject(serverInfo) || !SERVER_NAME.test(serverInfo.name)) { + throw new TypeError('Smithery release serverInfo must contain a safe server name.'); + } + if (typeof serverInfo.version !== 'string' || !SEMANTIC_VERSION.test(serverInfo.version)) { + throw new TypeError('Smithery release serverInfo must contain an exact semantic version.'); + } + if (!Array.isArray(tools) || tools.length === 0) { + throw new TypeError('Smithery release tools must be a non-empty array.'); + } + + const copiedTools = tools.map((tool) => { + if (!isObject(tool) || !TOOL_NAME.test(tool.name)) { + throw new TypeError('Every Smithery release tool must contain a safe name.'); + } + if (typeof tool.description !== 'string' || tool.description.length === 0) { + throw new TypeError('Every Smithery release tool must contain a description.'); + } + if (!isObject(tool.inputSchema) || tool.inputSchema.type !== 'object') { + throw new TypeError(`Smithery release tool ${tool.name} must contain an object inputSchema.`); + } + return structuredClone(tool); + }); + if (new Set(copiedTools.map(({ name }) => name)).size !== copiedTools.length) { + throw new TypeError('Smithery release tool names must be unique.'); + } + + return deepFreeze({ + type: 'stdio', + runtime: 'node', + serverCard: { + serverInfo: { name: serverInfo.name, version: serverInfo.version }, + tools: copiedTools, + }, + }); +} diff --git a/scripts/prove-smithery-bundle.mjs b/scripts/prove-smithery-bundle.mjs new file mode 100644 index 0000000..85b56ee --- /dev/null +++ b/scripts/prove-smithery-bundle.mjs @@ -0,0 +1,183 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { lstat, mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +import { runMcpb } from './lib/mcpb-cli.mjs'; +import { SMITHERY_QUALIFIED_NAME, smitheryBundleFilename } from './lib/smithery-bundle.mjs'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const source = JSON.parse(await readFile(join(root, 'smithery', 'source.json'), 'utf8')); +const defaultArtifact = join(root, 'artifacts', 'smithery', smitheryBundleFilename(source.version)); +const artifactPath = resolve(process.env.SMITHERY_MCPB_PATH ?? defaultArtifact); +const maximumBundleBytes = 25 * 1024 * 1024; +const maximumExtractedBytes = 100 * 1024 * 1024; +const maximumCapturedBytes = 256 * 1024; + +function sha256(buffer) { + return createHash('sha256').update(buffer).digest('hex'); +} + +function withDeadline(promise, label, timeoutMs = 30_000) { + let timeout; + return Promise.race([ + promise.finally(() => clearTimeout(timeout)), + new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs); + }), + ]); +} + +function parseToolResult(result) { + assert.equal(result.content.length, 1); + assert.equal(result.content[0].type, 'text'); + return JSON.parse(result.content[0].text); +} + +async function inspectExtractedTree(directory) { + let totalBytes = 0; + const topLevel = (await readdir(directory)).sort(); + assert.deepEqual(topLevel, ['LICENSE', 'README.md', 'manifest.json', 'server', 'source.json']); + + async function visit(current) { + for (const entry of await readdir(current, { withFileTypes: true })) { + const path = join(current, entry.name); + const pathStat = await lstat(path); + assert.equal( + pathStat.isSymbolicLink(), + false, + `MCPB contains symlink: ${relative(directory, path)}`, + ); + assert.doesNotMatch( + entry.name, + /^(?:\.env(?:\..*)?|\.npmrc)$/i, + 'MCPB contains a secret file', + ); + if (pathStat.isDirectory()) { + await visit(path); + } else { + assert.equal(pathStat.isFile(), true, `MCPB contains a non-file entry: ${path}`); + totalBytes += pathStat.size; + assert(totalBytes <= maximumExtractedBytes, 'MCPB extracted content is too large'); + } + } + } + + await visit(directory); +} + +const artifact = await readFile(artifactPath); +const artifactStat = await stat(artifactPath); +assert.equal(artifactStat.isFile(), true); +assert(artifactStat.size <= maximumBundleBytes, 'Smithery bundle is too large'); + +const localMetadataPath = `${defaultArtifact}.json`; +const expectedSha256 = process.env.SMITHERY_EXPECTED_SHA256 + ? process.env.SMITHERY_EXPECTED_SHA256 + : JSON.parse(await readFile(localMetadataPath, 'utf8')).sha256; +assert.match(expectedSha256, /^[0-9a-f]{64}$/); +assert.equal(sha256(artifact), expectedSha256, 'Smithery bundle SHA-256 changed'); + +const temporaryRoot = await mkdtemp(join(tmpdir(), 'buzzr-smithery-proof-')); +const unpackedDirectory = join(temporaryRoot, 'unpacked'); + +try { + await runMcpb(['unpack', artifactPath, unpackedDirectory], { + cwd: root, + env: { NPM_CONFIG_CACHE: join(temporaryRoot, 'mcpb-cache') }, + }); + await inspectExtractedTree(unpackedDirectory); + + const [manifest, bundleSource] = await Promise.all( + ['manifest.json', 'source.json'].map(async (filename) => + JSON.parse(await readFile(join(unpackedDirectory, filename), 'utf8')), + ), + ); + assert.equal(bundleSource.qualifiedName, SMITHERY_QUALIFIED_NAME); + assert.equal(bundleSource.version, source.version); + assert.equal(bundleSource.integrity, source.integrity); + assert.equal(manifest.version, source.version); + assert.equal(manifest.tools.length, 11); + assert.equal(manifest.server.type, 'node'); + assert.equal(manifest.server.entry_point, 'server/index.js'); + assert.doesNotMatch(JSON.stringify(manifest), /api.?key|password|secret|token/i); + + const entryPoint = resolve(unpackedDirectory, manifest.server.entry_point); + assert( + entryPoint.startsWith(`${resolve(unpackedDirectory)}${sep}`), + 'MCPB entry point escapes the bundle', + ); + const isolatedHome = join(temporaryRoot, 'home'); + const childEnvironment = Object.fromEntries( + ['PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT'].flatMap((key) => + process.env[key] ? [[key, process.env[key]]] : [], + ), + ); + Object.assign(childEnvironment, { + HOME: isolatedHome, + USERPROFILE: isolatedHome, + TMPDIR: join(temporaryRoot, 'tmp'), + TEMP: join(temporaryRoot, 'tmp'), + TMP: join(temporaryRoot, 'tmp'), + NO_COLOR: '1', + }); + + const transport = new StdioClientTransport({ + command: process.execPath, + args: [entryPoint], + cwd: unpackedDirectory, + env: childEnvironment, + stderr: 'pipe', + }); + let stderr = ''; + let stderrOverflow = false; + transport.stderr?.on('data', (chunk) => { + if (stderrOverflow) return; + const next = stderr + chunk.toString(); + if (Buffer.byteLength(next) > maximumCapturedBytes) { + stderrOverflow = true; + return; + } + stderr = next; + }); + + const client = new Client({ name: 'buzzr-smithery-proof', version: '1.0.0' }); + try { + await withDeadline(client.connect(transport), 'Smithery MCPB initialization'); + assert.deepEqual(client.getServerVersion(), { name: 'buzzr', version: source.version }); + + const listed = await withDeadline(client.listTools(), 'Smithery MCPB tools/list'); + assert.deepEqual( + listed.tools.map(({ name }) => name), + manifest.tools.map(({ name }) => name), + ); + + const fairLine = parseToolResult( + await withDeadline( + client.callTool({ + name: 'fair_line', + arguments: { selected: -110, opposite: -110, selectedSide: 'proof' }, + }), + 'Smithery MCPB fair_line call', + ), + ); + assert.equal(fairLine.selectedSide, 'proof'); + assert.equal(fairLine.fairProbability, 0.5); + assert.equal(fairLine.overround > 0, true); + } finally { + await withDeadline(client.close(), 'Smithery MCPB shutdown'); + } + + assert.equal(stderrOverflow, false); + assert.match(stderr, /buzzr MCP server v\S+ listening on stdio/); + process.stdout.write( + `Smithery MCPB ${source.version} passed exact hash, safe extraction, ${manifest.tools.length} tools, and real-client proof for ${bundleSource.qualifiedName}.\n`, + ); +} finally { + await rm(temporaryRoot, { recursive: true, force: true }); +} diff --git a/scripts/publish-smithery-bundle.mjs b/scripts/publish-smithery-bundle.mjs new file mode 100644 index 0000000..df3a6b4 --- /dev/null +++ b/scripts/publish-smithery-bundle.mjs @@ -0,0 +1,258 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve, sep } from 'node:path'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +import { runMcpb } from './lib/mcpb-cli.mjs'; +import { createSmitheryReleasePayload } from './lib/smithery-release.mjs'; +import { smitheryBundleFilename } from './lib/smithery-bundle.mjs'; + +const terminalStatuses = new Set([ + 'AUTH_REQUIRED', + 'AUTH_TIMEOUT', + 'CANCELLED', + 'FAILURE', + 'FAILURE_SCAN', + 'INTERNAL_ERROR', + 'SUCCESS', +]); +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const execFileAsync = promisify(execFile); +const source = JSON.parse(await readFile(join(root, 'smithery', 'source.json'), 'utf8')); +const artifactPath = join(root, 'artifacts', 'smithery', smitheryBundleFilename(source.version)); +const metadata = JSON.parse(await readFile(`${artifactPath}.json`, 'utf8')); +const artifact = await readFile(artifactPath); +const apiKey = process.env.SMITHERY_API_KEY; +const maximumBundleBytes = 25 * 1024 * 1024; + +assert( + typeof apiKey === 'string' && apiKey.length >= 20 && !/\s/.test(apiKey), + 'SMITHERY_API_KEY is required', +); +assert.equal(createHash('sha256').update(artifact).digest('hex'), metadata.sha256); +assert.equal(metadata.qualifiedName, source.qualifiedName); +assert.equal(metadata.toolCount, 11); +assert.equal(metadata.version, source.version); +assert.deepEqual(metadata.npm, { + package: source.package, + version: source.version, + gitHead: source.gitHead, + integrity: source.integrity, +}); +assert.equal(metadata.size, artifact.byteLength); +assert(artifact.byteLength <= maximumBundleBytes, 'Smithery bundle is too large'); + +async function smitheryRequest(path, options = {}) { + const response = await fetch(`https://api.smithery.ai${path}`, { + ...options, + headers: { + Authorization: `Bearer ${apiKey}`, + ...options.headers, + }, + signal: AbortSignal.timeout(options.timeout ?? 30_000), + }); + const text = await response.text(); + let body; + try { + body = text.length === 0 ? {} : JSON.parse(text); + } catch { + body = { error: text.slice(0, 2_000) }; + } + if (!response.ok) { + throw new Error( + `Smithery ${options.method ?? 'GET'} ${path} failed (${response.status}): ${JSON.stringify(body)}`, + ); + } + return body; +} + +async function downloadSmitheryBundle(path) { + const response = await fetch(`https://api.smithery.ai${path}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(120_000), + }); + if (!response.ok) { + const error = (await response.text()).slice(0, 2_000); + throw new Error(`Smithery GET ${path} failed (${response.status}): ${error}`); + } + const declaredLengthHeader = response.headers.get('content-length'); + if (declaredLengthHeader !== null) { + const declaredLength = Number(declaredLengthHeader); + assert( + Number.isFinite(declaredLength) && declaredLength <= maximumBundleBytes, + 'Published Smithery bundle declares an excessive size', + ); + } + assert(response.body, 'Published Smithery bundle has no response body'); + const reader = response.body.getReader(); + const chunks = []; + let totalBytes = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > maximumBundleBytes) { + await reader.cancel('Published Smithery bundle exceeded the byte limit'); + throw new Error('Published Smithery bundle is too large'); + } + chunks.push(Buffer.from(value)); + } + } finally { + reader.releaseLock(); + } + return Buffer.concat(chunks, totalBytes); +} + +function childEnvironment(temporaryRoot) { + const inherited = Object.fromEntries( + ['PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT'].flatMap((key) => + process.env[key] ? [[key, process.env[key]]] : [], + ), + ); + const home = join(temporaryRoot, 'home'); + const temporaryDirectory = join(temporaryRoot, 'tmp'); + return { + ...inherited, + HOME: home, + USERPROFILE: home, + TMPDIR: temporaryDirectory, + TEMP: temporaryDirectory, + TMP: temporaryDirectory, + NO_COLOR: '1', + }; +} + +const temporaryRoot = await mkdtemp(join(tmpdir(), 'buzzr-smithery-publish-')); +try { + const unpackedDirectory = join(temporaryRoot, 'unpacked'); + await mkdir(join(temporaryRoot, 'home'), { recursive: true }); + await mkdir(join(temporaryRoot, 'tmp'), { recursive: true }); + await runMcpb(['unpack', artifactPath, unpackedDirectory], { + cwd: root, + env: { NPM_CONFIG_CACHE: join(temporaryRoot, 'mcpb-cache') }, + }); + + const manifest = JSON.parse(await readFile(join(unpackedDirectory, 'manifest.json'), 'utf8')); + const bundleSource = JSON.parse(await readFile(join(unpackedDirectory, 'source.json'), 'utf8')); + assert.deepEqual(bundleSource, source, 'MCPB source record differs from committed source'); + assert.equal(manifest.name, 'buzzr-sports-engine'); + assert.equal(manifest.version, source.version); + assert.equal(manifest.tools.length, 11); + const installedManifest = JSON.parse( + await readFile( + join(unpackedDirectory, 'server', 'node_modules', ...source.package.split('/'), 'package.json'), + 'utf8', + ), + ); + assert.equal(installedManifest.name, source.package); + assert.equal(installedManifest.version, source.version); + assert.equal(installedManifest.gitHead, source.gitHead); + const entryPoint = resolve(unpackedDirectory, manifest.server.entry_point); + assert(entryPoint.startsWith(`${resolve(unpackedDirectory)}${sep}`)); + + const transport = new StdioClientTransport({ + command: process.execPath, + args: [entryPoint], + cwd: unpackedDirectory, + env: childEnvironment(temporaryRoot), + stderr: 'pipe', + }); + const client = new Client({ name: 'buzzr-smithery-publisher', version: '1.0.0' }); + let listed; + let serverInfo; + try { + await client.connect(transport); + serverInfo = client.getServerVersion(); + listed = await client.listTools(); + } finally { + await client.close(); + } + assert.deepEqual(serverInfo, { name: 'buzzr', version: source.version }); + assert.equal(listed.tools.length, 11); + const payload = createSmitheryReleasePayload({ serverInfo, tools: listed.tools }); + + const qualifiedName = encodeURIComponent(source.qualifiedName); + await smitheryRequest(`/servers/${qualifiedName}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + displayName: manifest.display_name, + description: manifest.description, + }), + }); + + const form = new FormData(); + form.append('payload', JSON.stringify(payload)); + form.append('bundle', new Blob([artifact], { type: 'application/octet-stream' }), 'server.mcpb'); + const accepted = await smitheryRequest(`/servers/${qualifiedName}/releases`, { + method: 'PUT', + body: form, + timeout: 120_000, + }); + const deploymentId = accepted.deploymentId ?? accepted.id; + assert.match(deploymentId, /^[0-9a-f-]{36}$/i); + process.stdout.write( + `${JSON.stringify({ stage: 'accepted', deploymentId, qualifiedName: source.qualifiedName })}\n`, + ); + + let release = accepted; + const deadline = Date.now() + 10 * 60_000; + while (!terminalStatuses.has(release.status)) { + assert(Date.now() < deadline, 'Smithery deployment timed out'); + await new Promise((resolveDelay) => setTimeout(resolveDelay, 2_000)); + release = await smitheryRequest(`/servers/${qualifiedName}/releases/${deploymentId}`); + } + assert.equal( + release.status, + 'SUCCESS', + `Smithery deployment failed: ${JSON.stringify(release.logs ?? [])}`, + ); + + const downloadedPath = join(temporaryRoot, 'published.mcpb'); + const downloaded = await downloadSmitheryBundle(`/servers/${qualifiedName}/download`); + assert.equal( + createHash('sha256').update(downloaded).digest('hex'), + metadata.sha256, + 'Published Smithery bundle differs from the uploaded bytes', + ); + await writeFile(downloadedPath, downloaded, { mode: 0o600 }); + assert.equal((await stat(downloadedPath)).isFile(), true); + const proofEnvironment = Object.fromEntries( + ['PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT'].flatMap((key) => + process.env[key] ? [[key, process.env[key]]] : [], + ), + ); + Object.assign(proofEnvironment, { + NO_COLOR: '1', + SMITHERY_EXPECTED_SHA256: metadata.sha256, + SMITHERY_MCPB_PATH: downloadedPath, + }); + const proof = await execFileAsync( + process.execPath, + [join(root, 'scripts', 'prove-smithery-bundle.mjs')], + { cwd: root, env: proofEnvironment, maxBuffer: 2 * 1024 * 1024 }, + ); + assert.match(proof.stdout, /passed exact hash, safe extraction, 11 tools/); + process.stdout.write( + `${JSON.stringify({ + stage: 'published', + deploymentId, + status: release.status, + qualifiedName: source.qualifiedName, + page: `https://smithery.ai/servers/${source.qualifiedName}`, + mcpUrl: release.mcpUrl ?? accepted.mcpUrl, + sha256: metadata.sha256, + toolCount: listed.tools.length, + })}\n`, + ); +} finally { + await rm(temporaryRoot, { recursive: true, force: true }); +} diff --git a/smithery/mcpb-manifest-v0.4.schema.json b/smithery/mcpb-manifest-v0.4.schema.json new file mode 100644 index 0000000..6e0295c --- /dev/null +++ b/smithery/mcpb-manifest-v0.4.schema.json @@ -0,0 +1,377 @@ +{ + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "dxt_version": { + "type": "string", + "const": "0.4", + "description": "@deprecated Use manifest_version instead" + }, + "manifest_version": { + "type": "string", + "const": "0.4" + }, + "name": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "repository": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "documentation": { + "type": "string", + "format": "uri" + }, + "support": { + "type": "string", + "format": "uri" + }, + "icon": { + "type": "string" + }, + "icons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "size": { + "type": "string", + "pattern": "^\\d+x\\d+$" + }, + "theme": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "src", + "size" + ], + "additionalProperties": false + } + }, + "screenshots": { + "type": "array", + "items": { + "type": "string" + } + }, + "localization": { + "type": "object", + "properties": { + "resources": { + "type": "string", + "pattern": "\\$\\{locale\\}" + }, + "default_locale": { + "type": "string", + "pattern": "^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$" + } + }, + "required": [ + "resources", + "default_locale" + ], + "additionalProperties": false + }, + "server": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "python", + "node", + "binary", + "uv" + ] + }, + "entry_point": { + "type": "string" + }, + "mcp_config": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "platform_overrides": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "command": { + "$ref": "#/properties/server/properties/mcp_config/properties/command" + }, + "args": { + "$ref": "#/properties/server/properties/mcp_config/properties/args" + }, + "env": { + "$ref": "#/properties/server/properties/mcp_config/properties/env" + } + }, + "additionalProperties": false + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "entry_point", + "mcp_config" + ], + "additionalProperties": false + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "tools_generated": { + "type": "boolean" + }, + "prompts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "arguments": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + } + }, + "required": [ + "name", + "text" + ], + "additionalProperties": false + } + }, + "prompts_generated": { + "type": "boolean" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "license": { + "type": "string" + }, + "privacy_policies": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "compatibility": { + "type": "object", + "properties": { + "claude_desktop": { + "type": "string" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "darwin", + "win32", + "linux" + ] + } + }, + "runtimes": { + "type": "object", + "properties": { + "python": { + "type": "string" + }, + "node": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "user_config": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "directory", + "file" + ] + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "multiple": { + "type": "boolean" + }, + "sensitive": { + "type": "boolean" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "required": [ + "type", + "title", + "description" + ], + "additionalProperties": false + } + }, + "_meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + } + }, + "required": [ + "name", + "version", + "description", + "author", + "server" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/smithery/package-lock.json b/smithery/package-lock.json new file mode 100644 index 0000000..d8c15b7 --- /dev/null +++ b/smithery/package-lock.json @@ -0,0 +1,1223 @@ +{ + "name": "buzzr-sports-engine-mcpb-runtime", + "version": "5.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "buzzr-sports-engine-mcpb-runtime", + "version": "5.1.0", + "dependencies": { + "@buzzr/mcp": "5.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@buzzr/bets-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@buzzr/bets-core/-/bets-core-5.0.0.tgz", + "integrity": "sha512-1ZOIsVEjdN4QfrwucMyayv+5gtq7ZBv84ZhCxArJbOGTp7jKqof2Zl0oHHNQOiRb6xBb/6ZZqNvXNGfGqIu1xg==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@buzzr/dfs-engine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@buzzr/dfs-engine/-/dfs-engine-5.1.0.tgz", + "integrity": "sha512-nvad/3xyzxjJA1/QQsNqfmYB9P8TIB1ErKSvhOWD1mL3FLzCe+HuNCM880dj8wOSglLUlZzFVXrn+VB/2/NQ3Q==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@buzzr/entertainment-engine": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@buzzr/entertainment-engine/-/entertainment-engine-5.0.0.tgz", + "integrity": "sha512-zVNLeDQGNi301d7W6twgKPezApmvJIMTEOHIZBfuGx1e9thyVGzJemUnehMHiqhB4YvFtIj0BTEYjZdN3hCetQ==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@buzzr/mcp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@buzzr/mcp/-/mcp-5.1.0.tgz", + "integrity": "sha512-5xWiYpL0CY4qtHWd9r23SnII4eBxFmr1D6jKilEV8BItleTsT3b8vBLr3F4id0YejlRGxPjeW/68Z4QCa1FzbA==", + "license": "MIT", + "dependencies": { + "@buzzr/bets-core": "5.0.0", + "@buzzr/dfs-engine": "5.1.0", + "@buzzr/entertainment-engine": "5.0.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.4.3" + }, + "bin": { + "buzzr-mcp": "dist/cli.js", + "mcp": "dist/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/smithery/package.json b/smithery/package.json new file mode 100644 index 0000000..5c29874 --- /dev/null +++ b/smithery/package.json @@ -0,0 +1,13 @@ +{ + "name": "buzzr-sports-engine-mcpb-runtime", + "version": "5.1.0", + "private": true, + "description": "Locked runtime dependency graph for the Buzzr Sports Engine Smithery MCPB.", + "type": "module", + "engines": { + "node": ">=22" + }, + "dependencies": { + "@buzzr/mcp": "5.1.0" + } +} diff --git a/smithery/source.json b/smithery/source.json new file mode 100644 index 0000000..1f9478f --- /dev/null +++ b/smithery/source.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "qualifiedName": "sarveshsea/buzzr-sports-engine", + "package": "@buzzr/mcp", + "version": "5.1.0", + "gitHead": "c26ad054f999ea726eb29c93d75e5a0dd8a7ea7e", + "integrity": "sha512-5xWiYpL0CY4qtHWd9r23SnII4eBxFmr1D6jKilEV8BItleTsT3b8vBLr3F4id0YejlRGxPjeW/68Z4QCa1FzbA==", + "tarball": "https://registry.npmjs.org/@buzzr/mcp/-/mcp-5.1.0.tgz", + "registry": "https://registry.npmjs.org/" +} diff --git a/tests/smithery-bundle.test.mjs b/tests/smithery-bundle.test.mjs new file mode 100644 index 0000000..42255a8 --- /dev/null +++ b/tests/smithery-bundle.test.mjs @@ -0,0 +1,232 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import test from 'node:test'; +import { promisify } from 'node:util'; +import { pathToFileURL } from 'node:url'; + +import { zipSync } from 'fflate'; + +const rootManifest = JSON.parse(await readFile('package.json', 'utf8')); +const rootLock = JSON.parse(await readFile('package-lock.json', 'utf8')); +const ciWorkflow = await readFile('.github/workflows/ci.yml', 'utf8'); +const mcpbTooling = await readFile('scripts/lib/mcpb-cli.mjs', 'utf8'); +const smitheryPublisher = await readFile('scripts/publish-smithery-bundle.mjs', 'utf8'); +const execFileAsync = promisify(execFile); + +const sampleTools = [ + { name: 'fair_line', description: 'Calculate a no-vig fair line.' }, + { name: 'grade_dfs_entry', description: 'Grade one DFS entry.' }, +]; + +test('creates a branded Smithery manifest without secrets or mutable caller data', async () => { + const { createSmitheryManifest } = await import('../scripts/lib/smithery-bundle.mjs'); + const manifest = createSmitheryManifest({ version: '5.1.0', tools: sampleTools }); + + assert.equal( + manifest.$schema, + 'https://raw.githubusercontent.com/modelcontextprotocol/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json', + ); + assert.equal(manifest.manifest_version, '0.4'); + assert.equal(manifest.name, 'buzzr-sports-engine'); + assert.equal(manifest.display_name, 'Buzzr Sports Engine'); + assert.equal(manifest.version, '5.1.0'); + assert.deepEqual(manifest.author, { + name: 'Sarvesh Chidambaram', + url: 'https://github.com/Buzzr-app', + }); + assert.deepEqual(manifest.repository, { + type: 'git', + url: 'https://github.com/Buzzr-app/dfs-engine', + }); + assert.deepEqual(manifest.server, { + type: 'node', + entry_point: 'server/index.js', + mcp_config: { + command: 'node', + args: ['${__dirname}/server/index.js'], + env: {}, + }, + }); + assert.deepEqual(manifest.compatibility, { + platforms: ['darwin', 'win32', 'linux'], + runtimes: { node: '>=22' }, + }); + assert.deepEqual(manifest.tools, sampleTools); + assert.equal(manifest.tools_generated, false); + assert.equal(manifest.user_config, undefined); + assert.doesNotMatch(JSON.stringify(manifest), /api.?key|password|secret|token/i); + + sampleTools[0].description = 'Caller mutation'; + assert.equal(manifest.tools[0].description, 'Calculate a no-vig fair line.'); +}); + +test('rejects unsafe Smithery manifest inputs', async () => { + const { createSmitheryManifest } = await import('../scripts/lib/smithery-bundle.mjs'); + + assert.throws( + () => createSmitheryManifest({ version: '../5.1.0', tools: sampleTools }), + /semantic version/i, + ); + assert.throws( + () => + createSmitheryManifest({ + version: '5.1.0', + tools: [ + { name: 'fair_line', description: 'One.' }, + { name: 'fair_line', description: 'Two.' }, + ], + }), + /unique/i, + ); + assert.throws( + () => + createSmitheryManifest({ + version: '5.1.0', + tools: [{ name: '../escape', description: 'Unsafe.' }], + }), + /tool name/i, + ); +}); + +test('derives a path-safe immutable MCPB filename from the exact version', async () => { + const { smitheryBundleFilename } = await import('../scripts/lib/smithery-bundle.mjs'); + + assert.equal(smitheryBundleFilename('5.1.0'), 'buzzr-sports-engine-5.1.0.mcpb'); + assert.throws(() => smitheryBundleFilename('../../escape'), /semantic version/i); +}); + +test('the release gate builds and proves the MCPB on every supported platform', () => { + assert.equal(rootManifest.scripts['build:mcpb'], 'node scripts/build-smithery-bundle.mjs'); + assert.equal(rootManifest.scripts['proof:mcpb'], 'node scripts/prove-smithery-bundle.mjs'); + assert.match(rootManifest.scripts.verify, /npm run build:mcpb && npm run proof:mcpb/); + assert.match( + ciWorkflow, + /mcp-packed-platforms:[\s\S]*run: npm run build:mcpb[\s\S]*run: npm run proof:mcpb/, + ); + assert.match( + ciWorkflow, + /os: \[ubuntu-latest, macos-latest, windows-latest\]/, + 'Linux, macOS, and Windows must all build and prove the MCPB', + ); +}); + +test('uses integrity-locked local MCPB tooling without a credential-bearing subprocess', () => { + assert.equal(rootManifest.devDependencies.fflate, '0.8.2'); + assert.deepEqual(rootLock.packages['node_modules/fflate'], { + version: '0.8.2', + resolved: 'https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz', + integrity: 'sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==', + dev: true, + license: 'MIT', + }); + assert.doesNotMatch(mcpbTooling, /npm exec|@anthropic-ai\/mcpb/); + assert.doesNotMatch(mcpbTooling, /\.\.\.process\.env/); + assert.doesNotMatch(mcpbTooling, /localeCompare/); +}); + +test('packs identical bytes across time zones', async () => { + const temporaryRoot = await mkdtemp(join(tmpdir(), 'buzzr-mcpb-timezone-')); + const sourceDirectory = join(temporaryRoot, 'source'); + await mkdir(sourceDirectory); + await Promise.all([ + writeFile(join(sourceDirectory, 'B.txt'), 'B\n'), + writeFile(join(sourceDirectory, 'a.txt'), 'a\n'), + ]); + const moduleUrl = pathToFileURL(resolve('scripts/lib/mcpb-cli.mjs')).href; + const script = `import { runMcpb } from ${JSON.stringify(moduleUrl)}; await runMcpb(['pack', process.argv[1], process.argv[2]]);`; + try { + const hashes = []; + for (const timeZone of ['UTC', 'America/Chicago']) { + const artifactPath = join(temporaryRoot, `${timeZone.replace('/', '-')}.mcpb`); + await execFileAsync( + process.execPath, + ['--input-type=module', '--eval', script, sourceDirectory, artifactPath], + { env: { TZ: timeZone }, maxBuffer: 1024 * 1024 }, + ); + hashes.push(createHash('sha256').update(await readFile(artifactPath)).digest('hex')); + } + assert.equal(hashes[0], hashes[1]); + } finally { + await rm(temporaryRoot, { recursive: true, force: true }); + } +}); + +test('pins the official MCPB v0.4 schema and preflights hostile archives', async () => { + const schema = await readFile('smithery/mcpb-manifest-v0.4.schema.json'); + assert.equal( + createHash('sha256').update(schema).digest('hex'), + '9e4fa3cdc4ae3872b3d76dd538a2517c4e9cf43a7ea2707819e11aedce09ee69', + ); + const { inspectMcpbArchive } = await import('../scripts/lib/mcpb-cli.mjs'); + assert.throws( + () => inspectMcpbArchive(zipSync({ '../escape.txt': new Uint8Array([1]) })), + /escapes|normalized/i, + ); + + const bomb = Buffer.from(zipSync({ 'safe.txt': new Uint8Array([1]) })); + const centralOffset = bomb.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])); + assert(centralOffset >= 0); + bomb.writeUInt32LE(100 * 1024 * 1024 + 1, centralOffset + 24); + assert.throws(() => inspectMcpbArchive(bomb), /declared content is too large/i); + + const symlink = Buffer.from(zipSync({ 'link.txt': new Uint8Array([1]) })); + const symlinkCentralOffset = symlink.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])); + symlink.writeUInt32LE((0o120777 << 16) >>> 0, symlinkCentralOffset + 38); + assert.throws(() => inspectMcpbArchive(symlink), /symbolic link/i); +}); + +test('builds the full Smithery stdio server card required by the release API', async () => { + const { createSmitheryReleasePayload } = await import('../scripts/lib/smithery-release.mjs'); + const tools = [ + { + name: 'fair_line', + description: 'Calculate a no-vig fair line.', + inputSchema: { + type: 'object', + properties: { selected: { type: 'number' } }, + required: ['selected'], + }, + }, + ]; + const payload = createSmitheryReleasePayload({ + serverInfo: { name: 'buzzr', version: '5.1.0' }, + tools, + }); + + assert.deepEqual(payload, { + type: 'stdio', + runtime: 'node', + serverCard: { + serverInfo: { name: 'buzzr', version: '5.1.0' }, + tools, + }, + }); + + tools[0].inputSchema.properties.selected.type = 'string'; + assert.equal(payload.serverCard.tools[0].inputSchema.properties.selected.type, 'number'); +}); + +test('rejects the incomplete MCPB tool summaries that Smithery release validation rejects', async () => { + const { createSmitheryReleasePayload } = await import('../scripts/lib/smithery-release.mjs'); + + assert.throws( + () => + createSmitheryReleasePayload({ + serverInfo: { name: 'buzzr', version: '5.1.0' }, + tools: [{ name: 'fair_line', description: 'Missing its input schema.' }], + }), + /inputSchema/i, + ); +}); + +test('the publisher downloads and re-proves the accepted registry bundle', () => { + assert.match(smitheryPublisher, /\/servers\/\$\{qualifiedName\}\/download/); + assert.match(smitheryPublisher, /SMITHERY_EXPECTED_SHA256/); + assert.match(smitheryPublisher, /prove-smithery-bundle\.mjs/); + assert.match(smitheryPublisher, /response\.body\.getReader\(\)/); + assert.doesNotMatch(smitheryPublisher, /response\.arrayBuffer\(\)/); +});