The procedure that actually compiles, passes every ratchet, and validates live. It tracks the real current API — verify each class/method before use (grep/Read, don't invent). Companion skills: edt-mcp-architecture (where things live), edt-mcp-tool-conventions (naming/errors/output), edt-mcp-bilingual (ru/en), edt-mcp-e2e-testing (e2e suite), edt-mcp-build-test (build).
-
Class in tools/impl/XxxTool.java: implements IMcpTool (read/action) or extends AbstractMetadataWriteTool for a tool that MUTATES the model — the base runs executeOnUiThread(params) on the UI thread (model mutation is safe only there), gives resolveProjectAndConfig(projectName) + unwrapCauseMessage(e) + default getResponseType()=JSON. tools/impl/ holds the tool only (utilities/bases → utils/, tools/base/). Declare public static final String NAME = "xxx"; (snake_case: get_/list_/create_/adopt_…).
-
IMcpTool surface:
getName() → NAME.
getDescription() — short, for the AI client (what it does + when). End with "… Full parameters and examples: call get_tool_guide('<name>')." (a unit test checks this; keep the description compact — the tools/list budget is shared).
getInputSchema() via JsonSchemaBuilder.object().stringProperty(name, desc[, required]).integerProperty(...).enumProperty(name, desc, "a","b").objectArrayProperty(...).build(). Required is the boolean 3rd argument of a property; there is NO .required(...) method. Parameter names are lowerCamelCase (ToolContractConsistencyTest fails snake_case). Canonical: projectName, fqn, modulePath, limit/offset (see edt-mcp-tool-conventions).
getOutputSchema() — declare the JSON result keys (for JSON tools).
- Guide — full how-to (parameters, examples, gotchas, how to revert), served on-demand through the guide-resource channel. Do NOT override
getGuide(): the default loads the bundled Markdown guides/<name>.md via GuideLoader. Author the guide as that file (one per tool); the body is the ## Guide section (the renderer adds the # <name> H1, description and the ## Parameters table itself, so use ## Parameter details for param prose). GuideCoverageTest fails the build if a registered tool has no guide. Override getGuide() only for a guide that must be computed at runtime.
getResponseType() — TEXT/JSON/MARKDOWN/YAML/IMAGE (may be omitted when extending AbstractMetadataWriteTool → JSON).
- Every parameter read in execute() must be in the schema, and vice versa.
-
execute / executeOnUiThread:
- Arguments —
JsonUtils.extractStringArgument/extractIntArgument/extractBooleanArgument/extractObjectArray; required ones — JsonUtils.requireArguments(params, "projectName", "fqn") (returns a ready error-JSON or null).
- Project/configuration —
ProjectContext.of(name) (utils) or resolveProjectAndConfig(name) (write base). NOT ResourcesPlugin.getWorkspace()… by hand.
- Model only inside a transaction boundary (hard rule, CLAUDE.md): reads in a read boundary, writes via
BmTransactions.write(...). A bare write only enqueues the async .mdo export — persist via BmTransactions.forceExportToDisk(project, [topObjectFqn, configurationFqn]) (pass the TOP FQN; for a member, its parent top; add the Configuration FQN = ((IBmObject)config).bmGetFqn() because its collection changed).
- Metadata resolution by FQN — use the SHARED resolvers (don't write the 47th copy):
MetadataTypeUtils.normalizeFqn/findObject (bilingual), MetadataNodeResolver.resolveExisting (existing) / resolveForCreate (new), FormStructureReader.resolveMdForm (forms — expects Type.Name.Forms.FormName or CommonForm.Name). Synonym is keyed by the language CODE (see edt-mcp-bilingual).
- EDT services: typed via
Activator.getDefault().getXxx() (getConfigurationProvider, getV8ProjectManager, getDtProjectManager, getBmModelManager, getMdRefactoringService) or ServiceAccess.get(IFoo.class) for a wired service (binding .toService()) — and add the package to MANIFEST Import-Package (step 5).
- Errors ONLY via
ToolResult.error(msg).toJson() (no exceptions escaping the tool, no bare "Error: …"). Success — ToolResult.success().put(k, v)….toJson(). An error must be actionable (name the bad value + how to fix / sibling tool).
-
Registration — in tools/BuiltInToolRegistrar: import …impl.XxxTool; + registry.register(new XxxTool()); (next to siblings).
-
MANIFEST.MF Import-Package — add each NEW imported com._1c.g5.* package (e.g. com._1c.g5.v8.dt.md.extension.adopt). A miss is a runtime ClassNotFound, not a compile error.
-
Unit test — MANDATORY (mcp/tests/.../tools/impl/XxxToolTest.java). BuiltInToolTestCoverageTest fails the build if a registered tool has no XxxToolTest. Contract (no live runtime): NAME, getResponseType(), description contains get_tool_guide('<name>'), schema contains every parameter, the required array is correct (and excludes optional ones), output-schema keys. Deeper behaviour is e2e.
-
e2e test — MANDATORY (tests/e2e/tools/test_<tool>.py). The e2e coverage ratchet fails if a tool in tools/list has none. Read edt-mcp-e2e-testing. happy + negative + error-quality; anti-cheat ("would the test fail if the tool were broken?"). A mutating tool: prefer non-mutating cases headless (the harness resets only the BASE fixture per-test, not the extension) + validate the mutating happy path LIVE; for a write-metadata tool that runs headless, check both the model read-back AND the on-disk structure (poll_diff_contains).
-
Golden — a new tool changes tools/list; regenerate and commit: EDT_MCP_UPDATE_GOLDEN=1 python tests/e2e/run_all.py --project TestConfiguration --filter test_tools_list_matches_committed_golden_snapshot, then git add tests/e2e/tools_list.golden.json.
-
README — bump the tool COUNT (two places), add the tool to its group table, to the flat tool table, and to the detailed section (parameters must match the schema).
-
Full contour (don't say "done" without it): bash source/compile.sh (compile + unit tests + every ratchet) → adversarial Opus review → redeploy to the dev EDT copy (edt-redeploy.ps1; the signal is the log line MCP server UP on 8765) → live validation against :8765 → commit. A freshly deployed tool is NOT in this session's MCP deferred list — to check it live, call it through the e2e harness client (python → harness.initialize(); harness.call("<name>", {...})) or Invoke-RestMethod, not the tool's MCP wrapper. See edt-mcp-build-test and the dev-loop memory.