Skip to content

The Engine takes injected rules, but there is no way for a user to supply one #16

Description

@royalpinto007

Problem

The rule engine was designed to take rules as input, and no user can supply any.

Engine in src/engine/engine.ts takes its rules by constructor injection:

export class Engine {
  private readonly rules: Rule[];
  constructor(rules: Rule[]) { this.rules = rules; }

but src/cli.ts only ever passes ALL_RULES from src/rules/index.ts, and McpAuditConfig in src/config.ts has no key for loading anything else. The config supports disabledRules, enabledRules, severityOverrides, failOn, and ignore: five ways to turn built-in rules down, and no way to add one.

So the extension point exists in the type signature, is exercised by test/engine.test.ts, and is unreachable for anyone who installs the package. The Rule type and createRuleContext are already the whole contract a third party rule would need.

Why it matters

Every linter that got adopted got adopted because teams could encode their own policy in it. The organization-specific rules are the ones nobody upstream will ever write: "no tool may be named deploy_* without a confirmation parameter", "every resource URI must be under our namespace", "this internal server must never expose a filesystem tool". Today the only options are to fork the package or to not use it.

It is also how the rule catalog grows. Contributors who write a rule for their own use are the natural source of the next built-in rule.

Suggested approach

  1. Add a rules (or plugins) key to McpAuditConfig: an array of module specifiers, resolved relative to the config file's directory.
  2. Load them with dynamic import() in loadConfig or a new loadRules module. Each module default-exports a Rule[]. Since the package is ESM, this is a few lines.
  3. Validate what comes back: every entry must have an id, a severity in ALL_SEVERITIES, a category, and an evaluate function. Reject a plugin whose rule id collides with a built-in one, or with another plugin, with a clear error naming both sources.
  4. Reserve the MCP id prefix for built-ins and require custom rules to use a different prefix, so the SARIF rule catalog stays unambiguous.
  5. Make sure the existing safety net still applies: Engine.run already wraps rule.evaluate in try/catch and converts a throw into an info finding, which is exactly the right behavior for third party code.
  6. Document it in the README with one complete worked example, and export the Rule and rule-context types from src/index.ts so plugin authors can type against them.
  7. Add a fixture plugin under fixtures/ and test: it loads, its findings appear, --disable can turn it off, severityOverrides applies to it, an id collision errors, and a throwing plugin does not crash the audit.

Done when

  • A user can add a rule from a config file without forking.
  • Plugin rules participate in --only, --disable, severityOverrides, and SARIF output exactly like built-ins.
  • Invalid or colliding plugins fail loudly at load time.
  • The README has a worked example.

Design first please: comment with the config shape and the module contract you propose before writing it, since this becomes a public interface the moment it ships.


If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions