Conventions for this repository. It holds two packages: packages/servicenow-mcp (the MCP server) and
packages/skills (the skill guides), plus .claude-plugin/marketplace.json at the root, which ships the
two together as a Claude Code plugin. There is nothing else.
- The default branch is
main. - Run
bun typecheckfrom the repo root; it runstsgoacross both packages via turbo. - Run tests from a package directory (
cd packages/servicenow-mcp && bun test), orbun run testfrom the root to run both through turbo.bun testat the root is blocked on purpose — seebunfig.toml.
Use conventional commit-style messages and PR titles: type(scope): summary.
Valid types are feat, fix, docs, chore, refactor, and test. Scopes are optional; use mcp,
skills, or ci when it helps.
Examples: fix(mcp): keep tenant scope on cached discovery results, docs: update contributing guide,
chore(skills): add fluent brownfield migration guide.
- Keep things in one function unless composable or reusable.
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
- Avoid
try/catchwhere possible. Prefer.catch(...). - Avoid the
anytype. - Use Bun APIs when possible, like
Bun.file(). - Rely on type inference; avoid explicit type annotations or interfaces unless necessary for exports or clarity.
- Prefer functional array methods (
flatMap,filter,map) overforloops; use type guards onfilterto keep inference downstream. - Add comments for non-obvious constraints and surprising behaviour, not for obvious assignments or control flow. A comment explaining why something is the way it is survives a rewrite; one restating the code does not.
Reduce total variable count by inlining when a value is only used once.
// Good
const journal = await Bun.file(path.join(dir, "journal.json")).json()
// Bad
const journalPath = path.join(dir, "journal.json")
const journal = await Bun.file(journalPath).json()Avoid unnecessary destructuring. Use dot notation to preserve context.
// Good
obj.a
obj.b
// Bad
const { a, b } = obj- Never alias imports. Do not use
import { foo as bar } from "...". - Never use star imports. Do not use
import * as Foo from "...". - Prefer dynamic imports for heavy modules only needed on selected code paths. Destructure the bindings near the top of the narrowest scope that needs them so they read like normal imports.
Prefer const over let. Use ternaries or early returns instead of reassignment.
// Good
const foo = condition ? 1 : 2
// Bad
let foo
if (condition) foo = 1
else foo = 2Avoid else. Prefer early returns.
// Good
function foo() {
if (condition) return 1
return 2
}
// Bad
function foo() {
if (condition) return 1
else return 2
}Make the main function read as the happy path and move supporting detail into small helpers below it.
// Good
export function loadThing(input: unknown) {
const config = requireConfig(input)
const metadata = readMetadata(input)
return createThing({ config, metadata })
}
function requireConfig(input: unknown) {
...
}Keep helpers close to the code they support, below the main export. Extract only when it names a real concept.
- Avoid mocks as much as possible. Test the actual implementation; do not duplicate its logic into the test.
- A test that only asserts what the code already says is worse than no test — it locks in behaviour without checking it. Prefer tests that would have caught a failure that really happened.
- Tests live beside the code in
__tests__/directories in the MCP package, and intest/in the skills package.
- One tool per file, under
src/servicenow-mcp-unified/tools/<domain>/snow_<name>.ts, exporting atoolDefinitionand anexecute(args, context). The domain'sindex.tsre-exports those as thesnow_<name>_def/snow_<name>_execpair the package publishes. - There is no central registry to edit:
script/generate-tools-json.tsdiscovers tools by walking that directory and reading each file'stoolDefinition. A file in the wrong shape is silently not a tool — which is why the generator refuses to write a manifest if any tool file fails to import, and reports what it skipped. - After adding or changing a tool, run
bun run --cwd packages/servicenow-mcp generate:tools-json.tools.jsonis fetched frommainby the public docs site, so a stale manifest means the tool exists but nobody can see it. - A new tool also has no entry in
sn-roles.manifest.json, and that one cannot be regenerated in CI — it reads a live instance's ACLs. Add the tool's name toAWAITING_PROBEinsrc/__tests__/sn-roles.test.ts; the nextprobe:sn-rolesrun covers it and takes it back off. - Every executor takes a
ServiceNowContext. In the HTTP transport one process serves many customers, so anything cached must be keyed bytenantId— a request that cannot be placed in a tenant is refused, not pooled. Seesrc/servicenow-mcp-unified/shared/tenant-scope.ts.
- One directory per skill, holding a single
SKILL.mdwith YAML frontmatter (name,description, and thesnow_*tools it expects). The directory name and the frontmatternamemust match. - After any change run
bun run --cwd packages/skills generateto refreshsrc/embedded.ts.bun testfails if it drifts, and fails if frontmatter names a tool the MCP server does not have. - Skill directory names are a contract with consumers that read the tree from a checkout. Renaming one is a breaking change for them, not a refactor.
packages/skillsis also the plugin source, so this tree is what/plugin install servicenow@seracinstalls: any directory here holding aSKILL.mdreaches installed users, with nothing listing it. The plugin's two manifests live inpackages/skills/.claude-plugin/andpackages/skills/.mcp.json.