-
Notifications
You must be signed in to change notification settings - Fork 2
mcp: best-practices resources + workflow instructions #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "exposed": { | ||
| "queries": { | ||
| "title": "GQ query best practices", | ||
| "description": "Read before authoring queries (via `read` or `change`). Covers .gq grammar, query name dispatch, parameter declaration, the PascalCase→lowerCamelCase edge casing trap, search functions requiring trailing `limit`, and the T12 non-nullable lint rule." | ||
| }, | ||
| "data": { | ||
| "title": "Data ingest patterns", | ||
| "description": "Read before calling `ingest`. Covers mode selection (merge/append/overwrite), the branch→ingest→verify→merge loop for large/risky writes, embedding-staleness gotcha (mode:merge does not recompute embeddings), and change-vs-ingest decision matrix." | ||
| }, | ||
| "schema": { | ||
| "title": "Schema authoring and evolution", | ||
| "description": "Read before calling `schema_apply`. Covers .pg grammar, decorators (@key, @unique, @embed, @rename_from), inline-only enums, the add-optional→backfill→tighten sequence for non-nullable additions, and why apply is main-only and rejects open feature branches." | ||
| }, | ||
| "remote-ops": { | ||
| "title": "Remote-operation safety", | ||
| "description": "Read after any 504 or unexpected error. Covers the verify-after-write ritual (commits_list head before/after), retry-safety table by node kind (pointer types dedupe via @key; append-only types duplicate on retry), and the `sync_branch()` server-internal error vs. a tool." | ||
| }, | ||
| "search": { | ||
| "title": "Vector and full-text search", | ||
| "description": "Read before using nearest/bm25/rrf. Covers the scope-first-rank-second pattern, the hardcoded embedding model (gemini-embedding-2-preview, Vector(3072)), and trailing-`limit` requirement." | ||
| } | ||
| }, | ||
| "skipped": { | ||
| "aliases": "CLI-only — omnigraph.yaml + --alias mechanics, not actionable through the HTTP API the MCP wraps.", | ||
| "commands": "CLI-only — the omnigraph binary's command surface, not the HTTP API.", | ||
| "server-policy": "Server-deployment concerns (Cedar policy, auth tokens), out of scope for an MCP client." | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| // Fetches the omnigraph-best-practices markdown references and bakes them | ||
| // into a generated TS module that the MCP server imports. The MCP exposes | ||
| // each file as a `omnigraph://best-practices/<topic>` resource so an LLM | ||
| // can pull the right reference on demand. | ||
| // | ||
| // Discovery is upstream-driven: we list the references/ directory on each | ||
| // run and require a matching curated entry in cookbook-descriptions.json | ||
| // for every file. If upstream adds a new reference without a matching | ||
| // description, the build fails loudly with a clear remediation message. | ||
| // If a description is stale (no longer matches upstream), the build also | ||
| // fails — we never ship descriptions for files that no longer exist. | ||
| // | ||
| // Why curated descriptions: the `description` is what the LLM reads at | ||
| // `resources/list` time to decide whether to pull a body. Auto-generated | ||
| // titles from filenames are too vague for that selection to be reliable. | ||
| // Hand-written guidance ("Read before authoring queries…") gives the LLM | ||
| // the cue it needs. | ||
| // | ||
| // Source of truth: ModernRelay/omnigraph-cookbooks @ main. The generated | ||
| // TS module is gitignored; every build/typecheck regenerates it. CI builds | ||
| // always fetch fresh; the published npm tarball ships the bundled JS with | ||
| // the markdown inlined as string constants. | ||
|
|
||
| import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const HERE = dirname(fileURLToPath(import.meta.url)); | ||
| const PKG_ROOT = dirname(HERE); | ||
| const OUT = join(PKG_ROOT, 'src/best-practices.gen.ts'); | ||
| const DESCRIPTIONS_PATH = join(PKG_ROOT, 'cookbook-descriptions.json'); | ||
|
|
||
| const REPO = 'ModernRelay/omnigraph-cookbooks'; | ||
| const REF = 'main'; | ||
| const REF_DIR = 'skills/omnigraph-best-practices/references'; | ||
|
|
||
| const LIST_URL = `https://api.github.com/repos/${REPO}/contents/${REF_DIR}?ref=${REF}`; | ||
| const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/${REF}/${REF_DIR}`; | ||
|
|
||
| interface CuratedDescription { | ||
| title: string; | ||
| description: string; | ||
| } | ||
|
|
||
| interface DescriptionsConfig { | ||
| /** Files to expose as MCP resources, with LLM-facing descriptions. */ | ||
| exposed: Record<string, CuratedDescription>; | ||
| /** Files intentionally not exposed; value is the reason. */ | ||
| skipped: Record<string, string>; | ||
| } | ||
|
|
||
| interface GithubContentItem { | ||
| name: string; | ||
| type: 'file' | 'dir' | 'symlink' | 'submodule'; | ||
| } | ||
|
|
||
| function constId(key: string): string { | ||
| // queries -> QUERIES_MD, remote-ops -> REMOTE_OPS_MD | ||
| return `${key.toUpperCase().replace(/-/g, '_')}_MD`; | ||
| } | ||
|
|
||
| async function listUpstream(): Promise<string[]> { | ||
| const res = await fetch(LIST_URL, { | ||
| headers: { Accept: 'application/vnd.github+json' }, | ||
| }); | ||
| if (!res.ok) { | ||
| throw new Error(`list ${LIST_URL}: ${res.status} ${res.statusText}`); | ||
| } | ||
| const items = (await res.json()) as GithubContentItem[]; | ||
| return items | ||
| .filter((i) => i.type === 'file' && i.name.endsWith('.md')) | ||
| .map((i) => i.name.replace(/\.md$/, '')) | ||
| .sort(); | ||
| } | ||
|
|
||
| async function fetchBody(key: string): Promise<string> { | ||
| const url = `${RAW_BASE}/${key}.md`; | ||
| const res = await fetch(url); | ||
| if (!res.ok) { | ||
| throw new Error(`fetch ${url}: ${res.status} ${res.statusText}`); | ||
| } | ||
| return await res.text(); | ||
| } | ||
|
|
||
| const upstream = await listUpstream(); | ||
| const config = JSON.parse(readFileSync(DESCRIPTIONS_PATH, 'utf8')) as DescriptionsConfig; | ||
|
|
||
| const exposedKeys = new Set(Object.keys(config.exposed)); | ||
| const skippedKeys = new Set(Object.keys(config.skipped)); | ||
| const knownKeys = new Set([...exposedKeys, ...skippedKeys]); | ||
| const remote = new Set(upstream); | ||
|
|
||
| const unaccounted = upstream.filter((k) => !knownKeys.has(k)); | ||
| const staleExposed = [...exposedKeys].filter((k) => !remote.has(k)); | ||
| const staleSkipped = [...skippedKeys].filter((k) => !remote.has(k)); | ||
| // A file declared in both sections is a contradictory state: the union | ||
| // hides it from the unaccounted check, then `exposed` silently wins at | ||
| // fetch time and the `skipped` reason becomes dead config. Fail loud so | ||
| // the maintainer picks one. | ||
| const contradictory = [...exposedKeys].filter((k) => skippedKeys.has(k)); | ||
|
|
||
| if ( | ||
| unaccounted.length > 0 || | ||
| staleExposed.length > 0 || | ||
| staleSkipped.length > 0 || | ||
| contradictory.length > 0 | ||
| ) { | ||
| const lines: string[] = ['cookbook-descriptions.json is out of sync with upstream:']; | ||
| if (unaccounted.length > 0) { | ||
| lines.push( | ||
| '', | ||
| 'New files upstream without a curated description or explicit skip:', | ||
| ...unaccounted.map((k) => ` + ${k}.md`), | ||
| '', | ||
| ` → in ${DESCRIPTIONS_PATH}, either:`, | ||
| ' - add to "exposed" with { title, description } if the file is', | ||
| ' useful to an LLM operating through the MCP. The description is', | ||
| ' what the LLM reads at resources/list time to decide whether', | ||
| ' to pull the body; write it for that audience.', | ||
| ' - or add to "skipped" with a one-line reason explaining why', | ||
| ' it is not actionable via the HTTP API the MCP wraps.', | ||
| ); | ||
| } | ||
| if (staleExposed.length > 0 || staleSkipped.length > 0) { | ||
| lines.push('', 'Entries that no longer exist upstream:'); | ||
| for (const k of staleExposed) lines.push(` - exposed.${k} (file gone)`); | ||
| for (const k of staleSkipped) lines.push(` - skipped.${k} (file gone)`); | ||
| lines.push('', ` → remove these from ${DESCRIPTIONS_PATH}`); | ||
| } | ||
| if (contradictory.length > 0) { | ||
| lines.push( | ||
| '', | ||
| 'Entries declared in BOTH "exposed" and "skipped":', | ||
| ...contradictory.map((k) => ` ! ${k}`), | ||
| '', | ||
| ` → in ${DESCRIPTIONS_PATH}, remove from one section.`, | ||
| ); | ||
| } | ||
| throw new Error(lines.join('\n')); | ||
| } | ||
|
|
||
| const exposedKeysSorted = upstream.filter((k) => exposedKeys.has(k)); | ||
| const fetched = await Promise.all( | ||
| exposedKeysSorted.map(async (key) => ({ | ||
| key, | ||
| constId: constId(key), | ||
| ...config.exposed[key]!, | ||
| body: await fetchBody(key), | ||
| })), | ||
| ); | ||
|
|
||
| const exports = fetched | ||
| .map((f) => `export const ${f.constId} = ${JSON.stringify(f.body)};`) | ||
| .join('\n\n'); | ||
|
|
||
| const indexLines = fetched | ||
| .map( | ||
| (f) => | ||
| ` { key: ${JSON.stringify(f.key)}, uri: ${JSON.stringify( | ||
| `omnigraph://best-practices/${f.key}`, | ||
| )}, title: ${JSON.stringify(f.title)}, description: ${JSON.stringify( | ||
| f.description, | ||
| )}, body: ${f.constId} },`, | ||
| ) | ||
| .join('\n'); | ||
|
|
||
| const out = `// AUTO-GENERATED by packages/mcp/scripts/sync-cookbook.ts. Do not edit by hand. | ||
| // Source: ${RAW_BASE} | ||
| // | ||
| // Discovery: every \`.md\` under the upstream references/ directory. | ||
| // Descriptions: packages/mcp/cookbook-descriptions.json (curated). | ||
| // | ||
| // Regenerated on every build/typecheck/test via the prebuild/pretypecheck/ | ||
| // pretest hooks in packages/mcp/package.json. The committed file is | ||
| // .gitignored — what ships in the npm tarball is the bundled JS with these | ||
| // strings inlined. | ||
|
|
||
| ${exports} | ||
|
|
||
| export interface CookbookEntry { | ||
| readonly key: string; | ||
| readonly uri: string; | ||
| readonly title: string; | ||
| readonly description: string; | ||
| readonly body: string; | ||
| } | ||
|
|
||
| export const COOKBOOK: readonly CookbookEntry[] = [ | ||
| ${indexLines} | ||
| ] as const; | ||
| `; | ||
|
|
||
| mkdirSync(dirname(OUT), { recursive: true }); | ||
| writeFileSync(OUT, out); | ||
| const totalBytes = fetched.reduce((n, f) => n + f.body.length, 0); | ||
| console.log( | ||
| `wrote ${OUT} (${fetched.length} files, ${totalBytes} chars of markdown, ${out.length} chars total)`, | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.