@contentrain/mcp 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/{annotations-Clhbw2lh.mjs → annotations-BFJbfSKe.mjs} +13 -1
- package/dist/{annotations-Clhbw2lh.mjs.map → annotations-BFJbfSKe.mjs.map} +1 -1
- package/dist/{apply-manager-D5ym5Jba.mjs → apply-manager-CDss8ZIr.mjs} +5 -5
- package/dist/{apply-manager-D5ym5Jba.mjs.map → apply-manager-CDss8ZIr.mjs.map} +1 -1
- package/dist/{branch-lifecycle-CFVpTpNT.mjs → branch-lifecycle-DjX3MXNG.mjs} +10 -7
- package/dist/{branch-lifecycle-CFVpTpNT.mjs.map → branch-lifecycle-DjX3MXNG.mjs.map} +1 -1
- package/dist/{config-BIl5Ybj0.mjs → config-Lw7vpK9n.mjs} +4 -2
- package/dist/config-Lw7vpK9n.mjs.map +1 -0
- package/dist/{context-J0OU9Nfp.mjs → context-DS40WOdw.mjs} +2 -2
- package/dist/{context-J0OU9Nfp.mjs.map → context-DS40WOdw.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +5 -5
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +1 -1
- package/dist/core/context.mjs +2 -2
- package/dist/core/doctor.mjs +3 -3
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +2 -2
- package/dist/{doctor-CgBoT6h1.mjs → doctor-Dxu_SzZy.mjs} +3 -3
- package/dist/{doctor-CgBoT6h1.mjs.map → doctor-Dxu_SzZy.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts.map +1 -1
- package/dist/git/branch-lifecycle.mjs +2 -2
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +3 -3
- package/dist/index.mjs +10 -10
- package/dist/{local-DvP4Y4T-.mjs → local-BEl5L7p5.mjs} +4 -4
- package/dist/{local-DvP4Y4T-.mjs.map → local-BEl5L7p5.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +60 -3
- package/dist/providers/github/index.d.mts.map +1 -1
- package/dist/providers/github/index.mjs +85 -5
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/providers/local/index.mjs +4 -4
- package/dist/server/http/index.d.mts +63 -21
- package/dist/server/http/index.d.mts.map +1 -1
- package/dist/server/http/index.mjs +165 -54
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-B1OJrUQ9.mjs → server-BiiVUjgs.mjs} +254 -48
- package/dist/server-BiiVUjgs.mjs.map +1 -0
- package/dist/server.mjs +10 -10
- package/dist/testing/conformance.d.mts +59 -0
- package/dist/testing/conformance.d.mts.map +1 -0
- package/dist/testing/conformance.mjs +59 -0
- package/dist/testing/conformance.mjs.map +1 -0
- package/dist/tools/annotations.d.mts.map +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/{transaction-DgkNgWSI.mjs → transaction-0D_edyh0.mjs} +48 -5
- package/dist/transaction-0D_edyh0.mjs.map +1 -0
- package/dist/{validator-B0nNRFeU.mjs → validator-BX2ou7V-.mjs} +46 -17
- package/dist/validator-BX2ou7V-.mjs.map +1 -0
- package/package.json +11 -6
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/content/blog/blog/en.json +6 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/context.json +22 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +7 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +16 -0
- package/testing/conformance/01-new-collection-entry/scenario.json +19 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +16 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/content/marketing/hero/en.json +4 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/context.json +22 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +5 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +16 -0
- package/testing/conformance/02-new-singleton/scenario.json +18 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +16 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/content/marketing/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/context.json +22 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/03-new-dictionary/scenario.json +19 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/README.md +77 -0
- package/testing/conformance/scenarios.json +36 -0
- package/dist/config-BIl5Ybj0.mjs.map +0 -1
- package/dist/server-B1OJrUQ9.mjs.map +0 -1
- package/dist/transaction-DgkNgWSI.mjs.map +0 -1
- package/dist/validator-B0nNRFeU.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\ninterface TreeEntry {\n path: string\n mode: '100644' | '100755' | '040000' | '160000' | '120000'\n type: 'blob' | 'tree' | 'commit'\n sha: string | null\n}\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Walk `input.changes` in parallel — create a blob per non-null\n * content, emit a tree entry with `sha: null` for each deletion.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = await Promise.all(\n input.changes.map(change => buildTreeEntry(client, repo, change)),\n )\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nasync function buildTreeEntry(\n client: GitHubClient,\n repo: RepoRef,\n change: FileChange,\n): Promise<TreeEntry> {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n const blob = await client.rest.git.createBlob({\n owner: repo.owner,\n repo: repo.name,\n content: change.content,\n encoding: 'utf-8',\n })\n return { path, mode: '100644', type: 'blob', sha: blob.data.sha }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return { merged: true, sha: response.data.sha, pullRequestUrl: null }\n } catch (error) {\n if (isNotModified(error)) {\n return { merged: true, sha: null, pullRequestUrl: null }\n }\n throw error\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n */\nexport class GitHubReader implements RepoReader {\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n ) {}\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Phase 5.1 ships with PAT auth only. App-based auth (JWT + installation\n * token) lands in phase 5.2 together with the HTTP transport.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n if (auth.type !== 'pat') {\n throw new Error(\n `GitHub auth type \"${auth.type}\" is not yet supported. Phase 5.1 ships with \"pat\" only; `\n + `\"app\" installation auth arrives in phase 5.2.`,\n )\n }\n\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n return new OctokitCtor({ auth: auth.token })\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'mcp@contentrain.io',\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\ninterface TreeEntry {\n path: string\n mode: '100644' | '100755' | '040000' | '160000' | '120000'\n type: 'blob' | 'tree' | 'commit'\n sha: string | null\n}\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Walk `input.changes` in parallel — create a blob per non-null\n * content, emit a tree entry with `sha: null` for each deletion.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = await Promise.all(\n input.changes.map(change => buildTreeEntry(client, repo, change)),\n )\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nasync function buildTreeEntry(\n client: GitHubClient,\n repo: RepoRef,\n change: FileChange,\n): Promise<TreeEntry> {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n const blob = await client.rest.git.createBlob({\n owner: repo.owner,\n repo: repo.name,\n content: change.content,\n encoding: 'utf-8',\n })\n return { path, mode: '100644', type: 'blob', sha: blob.data.sha }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return { merged: true, sha: response.data.sha, pullRequestUrl: null }\n } catch (error) {\n if (isNotModified(error)) {\n return { merged: true, sha: null, pullRequestUrl: null }\n }\n throw error\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n */\nexport class GitHubReader implements RepoReader {\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n ) {}\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'mcp@contentrain.io',\n}\n"],"mappings":"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"}
|
|
@@ -2,11 +2,11 @@ import "../../contracts-DfL0BfrD.mjs";
|
|
|
2
2
|
import "../../ops-DjEWi5Wa.mjs";
|
|
3
3
|
import "../../serialization-B1CEzR4H.mjs";
|
|
4
4
|
import "../../fs-DLbVB-Ek.mjs";
|
|
5
|
-
import "../../config-
|
|
5
|
+
import "../../config-Lw7vpK9n.mjs";
|
|
6
6
|
import { C as LocalReader } from "../../model-manager-BhLsUgaB.mjs";
|
|
7
7
|
import "../../meta-manager-C900rEyG.mjs";
|
|
8
|
-
import "../../context-
|
|
8
|
+
import "../../context-DS40WOdw.mjs";
|
|
9
9
|
import "../../id-CIO9s0x_.mjs";
|
|
10
|
-
import "../../transaction-
|
|
11
|
-
import { t as LocalProvider } from "../../local-
|
|
10
|
+
import "../../transaction-0D_edyh0.mjs";
|
|
11
|
+
import { t as LocalProvider } from "../../local-BEl5L7p5.mjs";
|
|
12
12
|
export { LocalProvider, LocalReader };
|
|
@@ -21,45 +21,87 @@ interface HttpMcpServerProviderOptions {
|
|
|
21
21
|
/** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */
|
|
22
22
|
path?: string;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Multi-tenant entry point — one resolver, many providers.
|
|
26
|
+
*
|
|
27
|
+
* Every new MCP session resolves its own `RepoProvider` from the incoming
|
|
28
|
+
* HTTP request (typically via a workspace / project identifier in a
|
|
29
|
+
* header or path segment). The resolver is invoked exactly once per
|
|
30
|
+
* session; follow-up requests carrying the `Mcp-Session-Id` header are
|
|
31
|
+
* routed to the existing `McpServer` + transport pair.
|
|
32
|
+
*
|
|
33
|
+
* Use this for Studio's hosted MCP Cloud or any agent that drives
|
|
34
|
+
* multiple Contentrain projects through a single HTTP endpoint.
|
|
35
|
+
*/
|
|
36
|
+
interface HttpMcpServerResolverOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Per-session provider resolver. Invoked on the first request of a
|
|
39
|
+
* new session (typically the MCP `initialize` call). Whatever
|
|
40
|
+
* provider the resolver returns is bound to that session for its
|
|
41
|
+
* lifetime.
|
|
42
|
+
*/
|
|
43
|
+
resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>;
|
|
44
|
+
/** Optional fallback project root injected into each resolved session. */
|
|
45
|
+
projectRoot?: string;
|
|
46
|
+
/** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */
|
|
47
|
+
authToken?: string;
|
|
48
|
+
/** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */
|
|
49
|
+
path?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Idle-session TTL in ms. Sessions that haven't received a request
|
|
52
|
+
* within this window are closed and their providers discarded.
|
|
53
|
+
* Defaults to 15 minutes.
|
|
54
|
+
*/
|
|
55
|
+
sessionTtlMs?: number;
|
|
56
|
+
}
|
|
24
57
|
interface HttpMcpServerHandle {
|
|
25
58
|
server: http.Server;
|
|
26
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The single shared `McpServer` when the server was started with
|
|
61
|
+
* `{ projectRoot }` or `{ provider }`. **Not set** in multi-tenant
|
|
62
|
+
* resolver mode — in that mode each session owns its own server and
|
|
63
|
+
* you should address them through the HTTP endpoint, not directly.
|
|
64
|
+
*/
|
|
65
|
+
mcp: McpServer | null;
|
|
27
66
|
url: string;
|
|
28
67
|
close: () => Promise<void>;
|
|
29
68
|
}
|
|
30
69
|
/**
|
|
31
70
|
* Start an HTTP transport for the MCP server on top of the local project.
|
|
32
71
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
72
|
+
* Stateless, single-endpoint `POST /mcp`. The underlying `McpServer` is
|
|
73
|
+
* the same one the stdio entry point uses, so every tool registered for
|
|
74
|
+
* stdio is available over HTTP with no duplication. Only the transport
|
|
75
|
+
* swaps out.
|
|
37
76
|
*
|
|
38
|
-
* Auth is a simple opaque Bearer token. When `authToken` is supplied,
|
|
39
|
-
* request without the exact `Authorization: Bearer <token>` header
|
|
40
|
-
* a 401 before the MCP transport sees it.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* GitHubProvider HTTP routing will land in phase 5.3 together with the
|
|
44
|
-
* tool-handler provider abstraction; today the HTTP server still drives
|
|
45
|
-
* a LocalProvider under the hood.
|
|
77
|
+
* Auth is a simple opaque Bearer token. When `authToken` is supplied,
|
|
78
|
+
* any request without the exact `Authorization: Bearer <token>` header
|
|
79
|
+
* gets a 401 before the MCP transport sees it. Non-local bind addresses
|
|
80
|
+
* should always set this — the `contentrain serve --mcpHttp` CLI hard-
|
|
81
|
+
* errors when it detects a non-localhost bind without a token.
|
|
46
82
|
*/
|
|
47
83
|
declare function startHttpMcpServer(opts: HttpMcpServerOptions & {
|
|
48
84
|
port: number;
|
|
49
85
|
host?: string;
|
|
50
86
|
}): Promise<HttpMcpServerHandle>;
|
|
51
87
|
/**
|
|
52
|
-
* Variant
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
88
|
+
* Variant accepting a pre-built provider or a per-request resolver.
|
|
89
|
+
*
|
|
90
|
+
* **Single-provider mode** (`{ provider }`):
|
|
91
|
+
* All sessions share one `McpServer` backed by the given provider.
|
|
92
|
+
* Good for CI runners, single-tenant deployments, or any case where
|
|
93
|
+
* the backing repository is fixed at boot time.
|
|
94
|
+
*
|
|
95
|
+
* **Multi-tenant mode** (`{ resolveProvider }`):
|
|
96
|
+
* Each session resolves its own provider from the incoming request.
|
|
97
|
+
* The resolver is invoked once per session; subsequent requests with
|
|
98
|
+
* the `Mcp-Session-Id` header reuse the same server + transport pair.
|
|
99
|
+
* Idle sessions are closed after `sessionTtlMs` (default 15m).
|
|
58
100
|
*/
|
|
59
|
-
declare function startHttpMcpServerWith(opts: HttpMcpServerProviderOptions & {
|
|
101
|
+
declare function startHttpMcpServerWith(opts: (HttpMcpServerProviderOptions | HttpMcpServerResolverOptions) & {
|
|
60
102
|
port: number;
|
|
61
103
|
host?: string;
|
|
62
104
|
}): Promise<HttpMcpServerHandle>;
|
|
63
105
|
//#endregion
|
|
64
|
-
export { type HttpMcpServerHandle, type HttpMcpServerOptions, type HttpMcpServerProviderOptions, startHttpMcpServer, startHttpMcpServerWith };
|
|
106
|
+
export { type HttpMcpServerHandle, type HttpMcpServerOptions, type HttpMcpServerProviderOptions, type HttpMcpServerResolverOptions, startHttpMcpServer, startHttpMcpServerWith };
|
|
65
107
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/server/http/server.ts"],"mappings":";;;;;UAOiB,oBAAA;;EAEf,WAAA;EAFe;EAIf,SAAA;;EAEA,IAAA;AAAA;AAAA,UAGe,4BAAA;EAHf;EAKA,QAAA,EAAU,YAAA;EALN;EAOJ,WAAA;EAJ2C;EAM3C,SAAA;EAJsB;EAMtB,IAAA;AAAA;AAAA,UAGe,mBAAA;EACf,MAAA,EAAQ,IAAA,CAAK,MAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/server/http/server.ts"],"mappings":";;;;;UAOiB,oBAAA;;EAEf,WAAA;EAFe;EAIf,SAAA;;EAEA,IAAA;AAAA;AAAA,UAGe,4BAAA;EAHf;EAKA,QAAA,EAAU,YAAA;EALN;EAOJ,WAAA;EAJ2C;EAM3C,SAAA;EAJsB;EAMtB,IAAA;AAAA;;;;;;AAeF;;;;;;;UAAiB,4BAAA;EAOuD;;;;;;EAAtE,eAAA,GAAkB,GAAA,EAAK,IAAA,CAAK,eAAA,KAAoB,YAAA,GAAe,OAAA,CAAQ,YAAA;EAAA;EAEvE,WAAA;EAEA;EAAA,SAAA;EAQA;EANA,IAAA;EAMY;AAGd;;;;EAHE,YAAA;AAAA;AAAA,UAGe,mBAAA;EACf,MAAA,EAAQ,IAAA,CAAK,MAAA;EAAb;;;;;;EAOA,GAAA,EAAK,SAAA;EACL,GAAA;EACA,KAAA,QAAa,OAAA;AAAA;AAmBf;;;;;;;;;;;;;;AAAA,iBAAsB,kBAAA,CACpB,IAAA,EAAM,oBAAA;EAAyB,IAAA;EAAc,IAAA;AAAA,IAC5C,OAAA,CAAQ,mBAAA;;;;;;;;;;;;;;;iBAgCW,sBAAA,CACpB,IAAA,GAAO,4BAAA,GAA+B,4BAAA;EAAkC,IAAA;EAAc,IAAA;AAAA,IACrF,OAAA,CAAQ,mBAAA"}
|
|
@@ -2,48 +2,51 @@ import "../../contracts-DfL0BfrD.mjs";
|
|
|
2
2
|
import "../../ops-DjEWi5Wa.mjs";
|
|
3
3
|
import "../../serialization-B1CEzR4H.mjs";
|
|
4
4
|
import "../../fs-DLbVB-Ek.mjs";
|
|
5
|
-
import "../../config-
|
|
5
|
+
import "../../config-Lw7vpK9n.mjs";
|
|
6
6
|
import "../../model-manager-BhLsUgaB.mjs";
|
|
7
7
|
import "../../meta-manager-C900rEyG.mjs";
|
|
8
|
-
import "../../context-
|
|
8
|
+
import "../../context-DS40WOdw.mjs";
|
|
9
9
|
import "../../id-CIO9s0x_.mjs";
|
|
10
|
-
import "../../transaction-
|
|
11
|
-
import "../../local-
|
|
10
|
+
import "../../transaction-0D_edyh0.mjs";
|
|
11
|
+
import "../../local-BEl5L7p5.mjs";
|
|
12
12
|
import "../../detect-DL9mFmaS.mjs";
|
|
13
|
-
import "../../branch-lifecycle-
|
|
14
|
-
import "../../annotations-
|
|
15
|
-
import { t as createServer } from "../../server-
|
|
16
|
-
import "../../validator-
|
|
13
|
+
import "../../branch-lifecycle-DjX3MXNG.mjs";
|
|
14
|
+
import "../../annotations-BFJbfSKe.mjs";
|
|
15
|
+
import { t as createServer } from "../../server-BiiVUjgs.mjs";
|
|
16
|
+
import "../../validator-BX2ou7V-.mjs";
|
|
17
17
|
import "../../scan-config-jC6XmiqF.mjs";
|
|
18
18
|
import "../../graph-builder-DXv2JEp-.mjs";
|
|
19
19
|
import "../../scanner-DKMwc03k.mjs";
|
|
20
20
|
import "../../tsx-parser-CQc6nr-o.mjs";
|
|
21
|
-
import "../../apply-manager-
|
|
22
|
-
import "../../doctor-
|
|
21
|
+
import "../../apply-manager-CDss8ZIr.mjs";
|
|
22
|
+
import "../../doctor-Dxu_SzZy.mjs";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import http from "node:http";
|
|
25
25
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
26
26
|
//#region src/server/http/server.ts
|
|
27
|
+
const MCP_SESSION_HEADER = "mcp-session-id";
|
|
27
28
|
/**
|
|
28
29
|
* Start an HTTP transport for the MCP server on top of the local project.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Stateless, single-endpoint `POST /mcp`. The underlying `McpServer` is
|
|
32
|
+
* the same one the stdio entry point uses, so every tool registered for
|
|
33
|
+
* stdio is available over HTTP with no duplication. Only the transport
|
|
34
|
+
* swaps out.
|
|
34
35
|
*
|
|
35
|
-
* Auth is a simple opaque Bearer token. When `authToken` is supplied,
|
|
36
|
-
* request without the exact `Authorization: Bearer <token>` header
|
|
37
|
-
* a 401 before the MCP transport sees it.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* GitHubProvider HTTP routing will land in phase 5.3 together with the
|
|
41
|
-
* tool-handler provider abstraction; today the HTTP server still drives
|
|
42
|
-
* a LocalProvider under the hood.
|
|
36
|
+
* Auth is a simple opaque Bearer token. When `authToken` is supplied,
|
|
37
|
+
* any request without the exact `Authorization: Bearer <token>` header
|
|
38
|
+
* gets a 401 before the MCP transport sees it. Non-local bind addresses
|
|
39
|
+
* should always set this — the `contentrain serve --mcpHttp` CLI hard-
|
|
40
|
+
* errors when it detects a non-localhost bind without a token.
|
|
43
41
|
*/
|
|
44
42
|
async function startHttpMcpServer(opts) {
|
|
43
|
+
const mcp = createServer(opts.projectRoot);
|
|
44
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
|
45
|
+
await mcp.connect(transport);
|
|
45
46
|
return startHttpMcpServerInternal({
|
|
46
|
-
|
|
47
|
+
mode: "single",
|
|
48
|
+
mcp,
|
|
49
|
+
transport,
|
|
47
50
|
port: opts.port,
|
|
48
51
|
host: opts.host,
|
|
49
52
|
authToken: opts.authToken,
|
|
@@ -51,82 +54,190 @@ async function startHttpMcpServer(opts) {
|
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
|
-
* Variant
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* Variant accepting a pre-built provider or a per-request resolver.
|
|
58
|
+
*
|
|
59
|
+
* **Single-provider mode** (`{ provider }`):
|
|
60
|
+
* All sessions share one `McpServer` backed by the given provider.
|
|
61
|
+
* Good for CI runners, single-tenant deployments, or any case where
|
|
62
|
+
* the backing repository is fixed at boot time.
|
|
63
|
+
*
|
|
64
|
+
* **Multi-tenant mode** (`{ resolveProvider }`):
|
|
65
|
+
* Each session resolves its own provider from the incoming request.
|
|
66
|
+
* The resolver is invoked once per session; subsequent requests with
|
|
67
|
+
* the `Mcp-Session-Id` header reuse the same server + transport pair.
|
|
68
|
+
* Idle sessions are closed after `sessionTtlMs` (default 15m).
|
|
60
69
|
*/
|
|
61
70
|
async function startHttpMcpServerWith(opts) {
|
|
71
|
+
if ("resolveProvider" in opts) return startMultiTenantHttpMcpServer(opts);
|
|
72
|
+
const mcp = createServer({
|
|
73
|
+
provider: opts.provider,
|
|
74
|
+
projectRoot: opts.projectRoot
|
|
75
|
+
});
|
|
76
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
|
77
|
+
await mcp.connect(transport);
|
|
62
78
|
return startHttpMcpServerInternal({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}),
|
|
79
|
+
mode: "single",
|
|
80
|
+
mcp,
|
|
81
|
+
transport,
|
|
67
82
|
port: opts.port,
|
|
68
83
|
host: opts.host,
|
|
69
84
|
authToken: opts.authToken,
|
|
70
85
|
path: opts.path
|
|
71
86
|
});
|
|
72
87
|
}
|
|
73
|
-
async function
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
88
|
+
async function startMultiTenantHttpMcpServer(opts) {
|
|
89
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
90
|
+
const ttl = opts.sessionTtlMs ?? 900 * 1e3;
|
|
91
|
+
const mountPath = opts.path ?? "/mcp";
|
|
92
|
+
const host = opts.host ?? "127.0.0.1";
|
|
93
|
+
async function disposeSession(sessionId) {
|
|
94
|
+
const session = sessions.get(sessionId);
|
|
95
|
+
if (!session) return;
|
|
96
|
+
sessions.delete(sessionId);
|
|
97
|
+
await session.transport.close().catch(() => {});
|
|
98
|
+
await session.mcp.close().catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
const reaper = setInterval(() => {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
for (const [id, session] of sessions) if (now - session.lastUsed > ttl) disposeSession(id);
|
|
103
|
+
}, Math.min(ttl, 6e4)).unref();
|
|
79
104
|
const server = http.createServer((req, res) => {
|
|
80
|
-
|
|
81
|
-
transport,
|
|
105
|
+
handleMultiTenantRequest(req, res, {
|
|
82
106
|
mountPath,
|
|
83
|
-
authToken:
|
|
107
|
+
authToken: opts.authToken,
|
|
108
|
+
resolveProvider: opts.resolveProvider,
|
|
109
|
+
projectRoot: opts.projectRoot,
|
|
110
|
+
sessions
|
|
84
111
|
});
|
|
85
112
|
});
|
|
86
113
|
await new Promise((resolve, reject) => {
|
|
87
114
|
server.once("error", reject);
|
|
88
|
-
server.listen(
|
|
115
|
+
server.listen(opts.port, host, () => {
|
|
89
116
|
server.off("error", reject);
|
|
90
117
|
resolve();
|
|
91
118
|
});
|
|
92
119
|
});
|
|
93
120
|
return {
|
|
94
121
|
server,
|
|
95
|
-
mcp,
|
|
122
|
+
mcp: null,
|
|
96
123
|
url: `http://${host}:${server.address().port}${mountPath}`,
|
|
97
124
|
close: async () => {
|
|
125
|
+
clearInterval(reaper);
|
|
98
126
|
await new Promise((resolve, reject) => {
|
|
99
127
|
server.close((err) => err ? reject(err) : resolve());
|
|
100
128
|
});
|
|
101
|
-
|
|
102
|
-
await
|
|
129
|
+
const ids = [...sessions.keys()];
|
|
130
|
+
await Promise.all(ids.map((id) => disposeSession(id)));
|
|
103
131
|
}
|
|
104
132
|
};
|
|
105
133
|
}
|
|
106
|
-
async function
|
|
134
|
+
async function handleMultiTenantRequest(req, res, ctx) {
|
|
135
|
+
if (!applyRouteChecks(req, res, {
|
|
136
|
+
mountPath: ctx.mountPath,
|
|
137
|
+
authToken: ctx.authToken
|
|
138
|
+
})) return;
|
|
139
|
+
const existingSessionId = pickSessionId(req.headers[MCP_SESSION_HEADER]);
|
|
140
|
+
if (existingSessionId && ctx.sessions.has(existingSessionId)) {
|
|
141
|
+
const session = ctx.sessions.get(existingSessionId);
|
|
142
|
+
session.lastUsed = Date.now();
|
|
143
|
+
try {
|
|
144
|
+
await session.transport.handleRequest(req, res);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
writeInternalError(res, error);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const mcp = createServer({
|
|
152
|
+
provider: await ctx.resolveProvider(req),
|
|
153
|
+
projectRoot: ctx.projectRoot
|
|
154
|
+
});
|
|
155
|
+
const transport = new StreamableHTTPServerTransport({
|
|
156
|
+
sessionIdGenerator: () => randomUUID(),
|
|
157
|
+
onsessioninitialized: (sessionId) => {
|
|
158
|
+
ctx.sessions.set(sessionId, {
|
|
159
|
+
mcp,
|
|
160
|
+
transport,
|
|
161
|
+
lastUsed: Date.now()
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
await mcp.connect(transport);
|
|
166
|
+
await transport.handleRequest(req, res);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
writeInternalError(res, error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function pickSessionId(header) {
|
|
172
|
+
if (!header) return void 0;
|
|
173
|
+
return Array.isArray(header) ? header[0] : header;
|
|
174
|
+
}
|
|
175
|
+
function applyRouteChecks(req, res, ctx) {
|
|
107
176
|
if (req.method !== "POST" && req.method !== "GET" && req.method !== "DELETE") {
|
|
108
177
|
writeJson(res, 405, { error: "Method Not Allowed" });
|
|
109
|
-
return;
|
|
178
|
+
return false;
|
|
110
179
|
}
|
|
111
180
|
if (!req.url || !req.url.startsWith(ctx.mountPath)) {
|
|
112
181
|
writeJson(res, 404, { error: "Not Found" });
|
|
113
|
-
return;
|
|
182
|
+
return false;
|
|
114
183
|
}
|
|
115
184
|
if (ctx.authToken) {
|
|
116
185
|
if (req.headers.authorization !== `Bearer ${ctx.authToken}`) {
|
|
117
186
|
writeJson(res, 401, { error: "Unauthorized" });
|
|
118
|
-
return;
|
|
187
|
+
return false;
|
|
119
188
|
}
|
|
120
189
|
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
async function startHttpMcpServerInternal(input) {
|
|
193
|
+
const { mcp, transport } = input;
|
|
194
|
+
const mountPath = input.path ?? "/mcp";
|
|
195
|
+
const host = input.host ?? "127.0.0.1";
|
|
196
|
+
const server = http.createServer((req, res) => {
|
|
197
|
+
handleRequest(req, res, {
|
|
198
|
+
transport,
|
|
199
|
+
mountPath,
|
|
200
|
+
authToken: input.authToken
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
server.once("error", reject);
|
|
205
|
+
server.listen(input.port, host, () => {
|
|
206
|
+
server.off("error", reject);
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
server,
|
|
212
|
+
mcp,
|
|
213
|
+
url: `http://${host}:${server.address().port}${mountPath}`,
|
|
214
|
+
close: async () => {
|
|
215
|
+
await new Promise((resolve, reject) => {
|
|
216
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
217
|
+
});
|
|
218
|
+
await transport.close().catch(() => {});
|
|
219
|
+
await mcp.close().catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async function handleRequest(req, res, ctx) {
|
|
224
|
+
if (!applyRouteChecks(req, res, {
|
|
225
|
+
mountPath: ctx.mountPath,
|
|
226
|
+
authToken: ctx.authToken
|
|
227
|
+
})) return;
|
|
121
228
|
try {
|
|
122
229
|
await ctx.transport.handleRequest(req, res);
|
|
123
230
|
} catch (error) {
|
|
124
|
-
|
|
125
|
-
error: "Internal Server Error",
|
|
126
|
-
message: error instanceof Error ? error.message : String(error)
|
|
127
|
-
});
|
|
231
|
+
writeInternalError(res, error);
|
|
128
232
|
}
|
|
129
233
|
}
|
|
234
|
+
function writeInternalError(res, error) {
|
|
235
|
+
if (res.headersSent) return;
|
|
236
|
+
writeJson(res, 500, {
|
|
237
|
+
error: "Internal Server Error",
|
|
238
|
+
message: error instanceof Error ? error.message : String(error)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
130
241
|
function writeJson(res, status, body) {
|
|
131
242
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
132
243
|
res.end(JSON.stringify(body));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["createMcpServer"],"sources":["../../../src/server/http/server.ts"],"sourcesContent":["import http from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type { AddressInfo } from 'node:net'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { createServer as createMcpServer, type ToolProvider } from '../../server.js'\n\nexport interface HttpMcpServerOptions {\n /** Project root the MCP server operates against (LocalProvider path). */\n projectRoot: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\nexport interface HttpMcpServerProviderOptions {\n /** Pre-built content provider (GitHubProvider, a mock, etc.). */\n provider: ToolProvider\n /** Local project root when the provider is a LocalProvider; optional otherwise. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\nexport interface HttpMcpServerHandle {\n server: http.Server\n mcp: McpServer\n url: string\n close: () => Promise<void>\n}\n\n/**\n * Start an HTTP transport for the MCP server on top of the local project.\n *\n * Phase 5.2 scope: stateless, single-endpoint `POST /mcp`. The underlying\n * McpServer is the same one the stdio entry point uses, so every tool\n * registered for stdio is available over HTTP with no duplication. Only\n * the transport swaps out.\n *\n * Auth is a simple opaque Bearer token. When `authToken` is supplied, any\n * request without the exact `Authorization: Bearer <token>` header gets\n * a 401 before the MCP transport sees it. Multi-tenant auth (per-project\n * API keys, GitHub App scopes) is Studio's concern — MCP stays minimal.\n *\n * GitHubProvider HTTP routing will land in phase 5.3 together with the\n * tool-handler provider abstraction; today the HTTP server still drives\n * a LocalProvider under the hood.\n */\nexport async function startHttpMcpServer(\n opts: HttpMcpServerOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n return startHttpMcpServerInternal({\n mcp: createMcpServer(opts.projectRoot),\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\n/**\n * Variant of {@link startHttpMcpServer} that accepts a pre-built provider\n * (and an optional `projectRoot`). This is the phase-5.3 entry point that\n * lets HTTP-hosted MCP route tool calls to a non-Local provider — e.g. a\n * `GitHubProvider` shared across many projects. Read-only tools work out\n * of the box; write-path tools return `capability_required: localWorktree`\n * when no `projectRoot` is supplied.\n */\nexport async function startHttpMcpServerWith(\n opts: HttpMcpServerProviderOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n return startHttpMcpServerInternal({\n mcp: createMcpServer({ provider: opts.provider, projectRoot: opts.projectRoot }),\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\nasync function startHttpMcpServerInternal(input: {\n mcp: McpServer\n port: number\n host?: string\n authToken?: string\n path?: string\n}): Promise<HttpMcpServerHandle> {\n const { mcp } = input\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n\n const mountPath = input.path ?? '/mcp'\n const host = input.host ?? '127.0.0.1'\n\n const server = http.createServer((req, res) => {\n void handleRequest(req, res, { transport, mountPath, authToken: input.authToken })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(input.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp,\n url,\n close: async () => {\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n await transport.close().catch(() => { /* best-effort */ })\n await mcp.close().catch(() => { /* best-effort */ })\n },\n }\n}\n\nasync function handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { transport: StreamableHTTPServerTransport, mountPath: string, authToken?: string },\n): Promise<void> {\n if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {\n writeJson(res, 405, { error: 'Method Not Allowed' })\n return\n }\n\n if (!req.url || !req.url.startsWith(ctx.mountPath)) {\n writeJson(res, 404, { error: 'Not Found' })\n return\n }\n\n if (ctx.authToken) {\n const header = req.headers.authorization\n if (header !== `Bearer ${ctx.authToken}`) {\n writeJson(res, 401, { error: 'Unauthorized' })\n return\n }\n }\n\n try {\n await ctx.transport.handleRequest(req, res)\n } catch (error) {\n if (!res.headersSent) {\n writeJson(res, 500, {\n error: 'Internal Server Error',\n message: error instanceof Error ? error.message : String(error),\n })\n }\n }\n}\n\nfunction writeJson(res: http.ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,eAAsB,mBACpB,MAC8B;AAC9B,QAAO,2BAA2B;EAChC,KAAKA,aAAgB,KAAK,YAAY;EACtC,MAAM,KAAK;EACX,MAAM,KAAK;EACX,WAAW,KAAK;EAChB,MAAM,KAAK;EACZ,CAAC;;;;;;;;;;AAWJ,eAAsB,uBACpB,MAC8B;AAC9B,QAAO,2BAA2B;EAChC,KAAKA,aAAgB;GAAE,UAAU,KAAK;GAAU,aAAa,KAAK;GAAa,CAAC;EAChF,MAAM,KAAK;EACX,MAAM,KAAK;EACX,WAAW,KAAK;EAChB,MAAM,KAAK;EACZ,CAAC;;AAGJ,eAAe,2BAA2B,OAMT;CAC/B,MAAM,EAAE,QAAQ;CAChB,MAAM,YAAY,IAAI,8BAA8B,EAClD,0BAA0B,YAAY,EACvC,CAAC;AACF,OAAM,IAAI,QAAQ,UAAU;CAE5B,MAAM,YAAY,MAAM,QAAQ;CAChC,MAAM,OAAO,MAAM,QAAQ;CAE3B,MAAM,SAAS,KAAK,cAAc,KAAK,QAAQ;AACxC,gBAAc,KAAK,KAAK;GAAE;GAAW;GAAW,WAAW,MAAM;GAAW,CAAC;GAClF;AAEF,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,KAAK,SAAS,OAAO;AAC5B,SAAO,OAAO,MAAM,MAAM,YAAY;AACpC,UAAO,IAAI,SAAS,OAAO;AAC3B,YAAS;IACT;GACF;AAKF,QAAO;EACL;EACA;EACA,KALU,UAAU,KAAK,GADX,OAAO,SAAS,CACM,OAAO;EAM3C,OAAO,YAAY;AACjB,SAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,WAAO,OAAM,QAAQ,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACpD;AACF,SAAM,UAAU,OAAO,CAAC,YAAY,GAAsB;AAC1D,SAAM,IAAI,OAAO,CAAC,YAAY,GAAsB;;EAEvD;;AAGH,eAAe,cACb,KACA,KACA,KACe;AACf,KAAI,IAAI,WAAW,UAAU,IAAI,WAAW,SAAS,IAAI,WAAW,UAAU;AAC5E,YAAU,KAAK,KAAK,EAAE,OAAO,sBAAsB,CAAC;AACpD;;AAGF,KAAI,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI,WAAW,IAAI,UAAU,EAAE;AAClD,YAAU,KAAK,KAAK,EAAE,OAAO,aAAa,CAAC;AAC3C;;AAGF,KAAI,IAAI;MACS,IAAI,QAAQ,kBACZ,UAAU,IAAI,aAAa;AACxC,aAAU,KAAK,KAAK,EAAE,OAAO,gBAAgB,CAAC;AAC9C;;;AAIJ,KAAI;AACF,QAAM,IAAI,UAAU,cAAc,KAAK,IAAI;UACpC,OAAO;AACd,MAAI,CAAC,IAAI,YACP,WAAU,KAAK,KAAK;GAClB,OAAO;GACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;GAChE,CAAC;;;AAKR,SAAS,UAAU,KAA0B,QAAgB,MAAqB;AAChF,KAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AAC7D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["createMcpServer"],"sources":["../../../src/server/http/server.ts"],"sourcesContent":["import http from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type { AddressInfo } from 'node:net'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { createServer as createMcpServer, type ToolProvider } from '../../server.js'\n\nexport interface HttpMcpServerOptions {\n /** Project root the MCP server operates against (LocalProvider path). */\n projectRoot: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\nexport interface HttpMcpServerProviderOptions {\n /** Pre-built content provider (GitHubProvider, a mock, etc.). */\n provider: ToolProvider\n /** Local project root when the provider is a LocalProvider; optional otherwise. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n}\n\n/**\n * Multi-tenant entry point — one resolver, many providers.\n *\n * Every new MCP session resolves its own `RepoProvider` from the incoming\n * HTTP request (typically via a workspace / project identifier in a\n * header or path segment). The resolver is invoked exactly once per\n * session; follow-up requests carrying the `Mcp-Session-Id` header are\n * routed to the existing `McpServer` + transport pair.\n *\n * Use this for Studio's hosted MCP Cloud or any agent that drives\n * multiple Contentrain projects through a single HTTP endpoint.\n */\nexport interface HttpMcpServerResolverOptions {\n /**\n * Per-session provider resolver. Invoked on the first request of a\n * new session (typically the MCP `initialize` call). Whatever\n * provider the resolver returns is bound to that session for its\n * lifetime.\n */\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n /** Optional fallback project root injected into each resolved session. */\n projectRoot?: string\n /** Optional Bearer token — when set, requests must send `Authorization: Bearer <token>`. */\n authToken?: string\n /** Mount path for the MCP JSON-RPC endpoint. Defaults to `/mcp`. */\n path?: string\n /**\n * Idle-session TTL in ms. Sessions that haven't received a request\n * within this window are closed and their providers discarded.\n * Defaults to 15 minutes.\n */\n sessionTtlMs?: number\n}\n\nexport interface HttpMcpServerHandle {\n server: http.Server\n /**\n * The single shared `McpServer` when the server was started with\n * `{ projectRoot }` or `{ provider }`. **Not set** in multi-tenant\n * resolver mode — in that mode each session owns its own server and\n * you should address them through the HTTP endpoint, not directly.\n */\n mcp: McpServer | null\n url: string\n close: () => Promise<void>\n}\n\nconst MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * Start an HTTP transport for the MCP server on top of the local project.\n *\n * Stateless, single-endpoint `POST /mcp`. The underlying `McpServer` is\n * the same one the stdio entry point uses, so every tool registered for\n * stdio is available over HTTP with no duplication. Only the transport\n * swaps out.\n *\n * Auth is a simple opaque Bearer token. When `authToken` is supplied,\n * any request without the exact `Authorization: Bearer <token>` header\n * gets a 401 before the MCP transport sees it. Non-local bind addresses\n * should always set this — the `contentrain serve --mcpHttp` CLI hard-\n * errors when it detects a non-localhost bind without a token.\n */\nexport async function startHttpMcpServer(\n opts: HttpMcpServerOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const mcp = createMcpServer(opts.projectRoot)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\n/**\n * Variant accepting a pre-built provider or a per-request resolver.\n *\n * **Single-provider mode** (`{ provider }`):\n * All sessions share one `McpServer` backed by the given provider.\n * Good for CI runners, single-tenant deployments, or any case where\n * the backing repository is fixed at boot time.\n *\n * **Multi-tenant mode** (`{ resolveProvider }`):\n * Each session resolves its own provider from the incoming request.\n * The resolver is invoked once per session; subsequent requests with\n * the `Mcp-Session-Id` header reuse the same server + transport pair.\n * Idle sessions are closed after `sessionTtlMs` (default 15m).\n */\nexport async function startHttpMcpServerWith(\n opts: (HttpMcpServerProviderOptions | HttpMcpServerResolverOptions) & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n if ('resolveProvider' in opts) {\n return startMultiTenantHttpMcpServer(opts)\n }\n const mcp = createMcpServer({ provider: opts.provider, projectRoot: opts.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n })\n await mcp.connect(transport)\n return startHttpMcpServerInternal({\n mode: 'single',\n mcp,\n transport,\n port: opts.port,\n host: opts.host,\n authToken: opts.authToken,\n path: opts.path,\n })\n}\n\ninterface MultiTenantSession {\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n lastUsed: number\n}\n\nasync function startMultiTenantHttpMcpServer(\n opts: HttpMcpServerResolverOptions & { port: number, host?: string },\n): Promise<HttpMcpServerHandle> {\n const sessions = new Map<string, MultiTenantSession>()\n const ttl = opts.sessionTtlMs ?? 15 * 60 * 1000\n const mountPath = opts.path ?? '/mcp'\n const host = opts.host ?? '127.0.0.1'\n\n async function disposeSession(sessionId: string): Promise<void> {\n const session = sessions.get(sessionId)\n if (!session) return\n sessions.delete(sessionId)\n await session.transport.close().catch(() => { /* best-effort */ })\n await session.mcp.close().catch(() => { /* best-effort */ })\n }\n\n const reaper = setInterval(() => {\n const now = Date.now()\n for (const [id, session] of sessions) {\n if (now - session.lastUsed > ttl) void disposeSession(id)\n }\n }, Math.min(ttl, 60_000)).unref()\n\n const server = http.createServer((req, res) => {\n void handleMultiTenantRequest(req, res, {\n mountPath,\n authToken: opts.authToken,\n resolveProvider: opts.resolveProvider,\n projectRoot: opts.projectRoot,\n sessions,\n })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(opts.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp: null,\n url,\n close: async () => {\n clearInterval(reaper)\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n const ids = [...sessions.keys()]\n await Promise.all(ids.map(id => disposeSession(id)))\n },\n }\n}\n\nasync function handleMultiTenantRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: {\n mountPath: string\n authToken?: string\n resolveProvider: (req: http.IncomingMessage) => ToolProvider | Promise<ToolProvider>\n projectRoot?: string\n sessions: Map<string, MultiTenantSession>\n },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n const existingSessionId = pickSessionId(req.headers[MCP_SESSION_HEADER])\n if (existingSessionId && ctx.sessions.has(existingSessionId)) {\n const session = ctx.sessions.get(existingSessionId)!\n session.lastUsed = Date.now()\n try {\n await session.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n return\n }\n\n try {\n const provider = await ctx.resolveProvider(req)\n const mcp = createMcpServer({ provider, projectRoot: ctx.projectRoot })\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sessionId: string) => {\n ctx.sessions.set(sessionId, { mcp, transport, lastUsed: Date.now() })\n },\n })\n await mcp.connect(transport)\n await transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction pickSessionId(header: string | string[] | undefined): string | undefined {\n if (!header) return undefined\n return Array.isArray(header) ? header[0] : header\n}\n\nfunction applyRouteChecks(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { mountPath: string, authToken?: string },\n): boolean {\n if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {\n writeJson(res, 405, { error: 'Method Not Allowed' })\n return false\n }\n if (!req.url || !req.url.startsWith(ctx.mountPath)) {\n writeJson(res, 404, { error: 'Not Found' })\n return false\n }\n if (ctx.authToken) {\n const header = req.headers.authorization\n if (header !== `Bearer ${ctx.authToken}`) {\n writeJson(res, 401, { error: 'Unauthorized' })\n return false\n }\n }\n return true\n}\n\nasync function startHttpMcpServerInternal(input: {\n mode: 'single'\n mcp: McpServer\n transport: StreamableHTTPServerTransport\n port: number\n host?: string\n authToken?: string\n path?: string\n}): Promise<HttpMcpServerHandle> {\n const { mcp, transport } = input\n const mountPath = input.path ?? '/mcp'\n const host = input.host ?? '127.0.0.1'\n\n const server = http.createServer((req, res) => {\n void handleRequest(req, res, { transport, mountPath, authToken: input.authToken })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(input.port, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo\n const url = `http://${host}:${address.port}${mountPath}`\n\n return {\n server,\n mcp,\n url,\n close: async () => {\n await new Promise<void>((resolve, reject) => {\n server.close(err => (err ? reject(err) : resolve()))\n })\n await transport.close().catch(() => { /* best-effort */ })\n await mcp.close().catch(() => { /* best-effort */ })\n },\n }\n}\n\nasync function handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ctx: { transport: StreamableHTTPServerTransport, mountPath: string, authToken?: string },\n): Promise<void> {\n if (!applyRouteChecks(req, res, { mountPath: ctx.mountPath, authToken: ctx.authToken })) return\n\n try {\n await ctx.transport.handleRequest(req, res)\n } catch (error) {\n writeInternalError(res, error)\n }\n}\n\nfunction writeInternalError(res: http.ServerResponse, error: unknown): void {\n if (res.headersSent) return\n writeJson(res, 500, {\n error: 'Internal Server Error',\n message: error instanceof Error ? error.message : String(error),\n })\n}\n\nfunction writeJson(res: http.ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n"],"mappings":"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"}
|