Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ packages/*/docs
*.log
.DS_Store
.vitest-cache
artifacts/

# Local agent tooling
.claude/
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion docs/security-and-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
182 changes: 182 additions & 0 deletions scripts/build-smithery-bundle.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
12 changes: 12 additions & 0 deletions scripts/check-mcp-client-skill-contracts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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`/,
Expand Down
Loading
Loading