DeepSeek Harness plugin registers zero tools: json schema
A plugin shows as mounted with a normal status, yet the tool list is empty and console.warn never fires; first check whether a hand-written register() uses { type: 'json' } for output.schema. According to Discussion #9875, json is not in the output whitelist SCHEMA_TYPES, so registration-time validation throws, and the cordis fiber catches the throw and sends it to context.logger.error. When the deployment has no sink for that level, the result is "zero tools, zero notice". The plugin did load; its tools were rejected.
Three facts behind a DeepSeek Harness plugin with zero tools
Only when all three hold is this a rejected registration rather than a plugin that never started. Confirm them one by one so this does not get confused with "the plugin is not loaded":
- The plugin appears in the loaded list and the profile reports no load failure. Expected:
applywas called. - Call
ctx.tools.schemas()or inspect the tool list. Expected: the count is zero, so registration left no tools behind. - The
console.warncall count is zero and the interface shows no red error. Expected: the failure was swallowed by the logging layer. - Your plugin registers four tools and all of them disappear. Expected: the whole batch aborted at the first bad schema rather than a single tool being skipped.
What the model sees with a DeepSeek Harness empty registry
If the tools never enter the registry, the model does not even know they exist, so it does not error; it routes around them. This explains why users perceive "the feature does not work" rather than "the plugin is broken":
- The tool list in the system prompt is empty. Expected: the model has nothing to call.
- The user asks for the corresponding operation. Expected: the model answers in text or switches to another tool.
- The plugin detail page still shows it as loaded. Expected: troubleshooting drifts toward permissions or networking.
- No exception surfaces. Expected: reinstalling the plugin changes nothing unless the schema changes.
logger.erroris connected to a visible sink. Expected: startup shows "tool rejected" instead of silence.
DeepSeek Harness call chain: where register throws and who swallows it
According to the community's source review in the thread, the failure happens in the registration-time schema assertion, not during tool execution. For the full tool contract, see tool plugin development; five steps cover the failure:
register()first checks whetheroutput.renderexists. Expected: the tool is rejected when it is missing.- It then calls
assertSupportedJsonSchema(output.schema). Expected: the output schema goes through the whitelist check. SCHEMA_TYPESlists onlyobject,array,string,number,integer,boolean, andnull. Expected:jsonis absent, so validation raises a violation.- The throw bubbles up to the plugin's
apply. Expected: the cordis fiber captures it and passes it tocontext.logger.error. - The deployment has no sink for that level. Expected: the plugin is mounted, the registry is empty, and the console is quiet at the same time.
DSH plugin fixes: defineTool, empty schema, or surface the error
All three paths get the tools registered again; they differ in whether you change the authoring style, one schema, or the visibility of the failure. Fix the authoring style first, then add an error sink so the next case is not silent.
Path 1: switch to defineTool so json goes through compilation
This is the least burdensome fix for authors: give the "compile the schema" step back to DSH instead of writing a wire schema by hand. According to the discussion, defineTool's valueSchemaSpec path already accepts json and compiles it to {}.
- Find the hand-written
ctx.tools.register({...})call. Expected: the tool definition site is identified. - Replace it with a
defineToolwrapper, passing the name, description, parameters, and output spec. Expected: the tool is defined through the wrapper. - Keep
{ type: 'json' }in the output. Expected:defineToolconverts it to an unconstrained schema during compilation. - Reload the plugin and call
ctx.tools.schemas(). Expected: the tool count is restored and the output no longer triggers aSCHEMA_TYPESviolation.
const tool = defineTool({
name: "read_meta",
description: "Read plugin metadata",
parameters: { type: "object", properties: {} },
output: { type: "json" },
})
Path 2: use {} for unconstrained output with register
If you must keep a hand-written register(), replace { type: 'json' } with {}, which is the compiled equivalent. The empty object means "any JSON value" and is the form the validator accepts.
- Locate the
output.schemafield. Expected: the output contract is visible. - Remove
type: 'json'and use the empty object{}. Expected: the schema becomes an empty object. - If the old form expressed a nested structure, use
type: 'object'withproperties, or keep{}and narrow it insiderender. Expected: the schema either expresses the nested shape or no constraint. - Reload and confirm the
ctx.tools.schemas()count. Expected: registration succeeds and the model receives a valid schema with no type constraint.
ctx.tools.register({
name: "read_meta",
render: renderMeta,
parameters: { type: "object", properties: {} },
output: { schema: {} },
})
Path 3: surface registration rejections in the UI or startup log
The first two paths fix one plugin; this one fixes "the next one will be silent too". According to the reply in the thread, registration already treats a contract violation as a throw, so the call site can distinguish "rejected" from "there were never any tools".
- Count rejections on the
register()call site. Expected: the number of rejected tools is known. - Print a summary line when plugin startup completes. Expected: startup logs show the count.
- Connect that count to the DSH plugin market detail page or the system log panel. Expected: users can see "loaded but zero tools".
- Add the available alternative to the thrown message: use
{}for unconstrained output. Expected: the error tells the author what to write instead. - Expected result: this class of error no longer requires reading source to locate, and the market page can flag an abnormal state.
DeepSeek Harness plugin registration self-check
Testing through the real registration path is more useful than 282 green tests against a stub. The thread notes that a stub register accepts anything, which is exactly why "all tests green, plugin with zero tools" can coexist.
- Create a real cordis Context and ToolRuntime in the test. Expected: the test uses the production registration path.
- Register the tool through the real
defineToolorregister()path. Expected: no stub stands between the tool and the runtime. - Assert that the
ctx.tools.schemas()count matches the expectation. Expected: a mismatch fails the test. - Build a separate case with
{ type: 'json' }. Expected: it is either compiled or rejected with a readable error. - Assert both the rejection count and the error text. Expected: the problem fails in CI instead of surfacing after a user installs the plugin.
Source: Discussion #9875.
FAQ
In DeepSeek Harness, first check whether the plugin calls register() by hand and writes type: json in its output schema. According to Discussion #9875, SCHEMA_TYPES contains only object, array, string, number, integer, boolean, and null; json is not among them, so registration throws. The plugin mounts successfully, but no tool is registered, and ctx.tools.schemas() confirms the count is zero.
In DeepSeek Harness the throw happens inside register(), and the catch is outside the plugin's apply. The cordis fiber catches the thrown apply and hands the error to context.logger.error; whether it is visible depends on whether the deployment configures a sink. Without a sink, the result is 'mounted, zero tools, zero notice'.
DeepSeek Harness's register() receives an already compiled wire schema, and the validator only accepts standard JSON Schema types. According to the maintainer's explanation in the thread, json is author-side syntax sugar for defineTool and compiles to {}; a hand-written register() has no compilation step, so writing json is out of bounds. Unconstrained output should be written as an empty object {}.
DeepSeek Harness's SCHEMA_TYPES is the wire-schema type table sent to model APIs, and json is not a JSON Schema keyword. According to the reply in the thread, adding it would make the validator accept a type that does not exist and break the layer that checks the external wire format. The right move is for author-side syntax sugar to compile to {}.
Related Terms
- SCHEMA_TYPES
- SCHEMA_TYPES is the whitelist of JSON Schema types allowed in a wire schema by the DSH tool runtime. It currently contains object, array, string, number, integer, boolean, and null, but not json, so a hand-written schema using json is rejected at registration.— https://github.com/deepseek-ai/deepseek-harness/discussions/9875
- register()
- `register()` is the entry point that attaches a tool definition to the DSH ToolRuntime; it receives compiled `parameters` and `output.schema`. At registration time it calls `assertSupportedJsonSchema` to validate the output contract and throws on a violation.— https://github.com/deepseek-ai/deepseek-harness/discussions/9875
- defineTool
- `defineTool` is DSH's tool wrapper for plugin authors; it accepts an author-side schema in `valueSchemaSpec` form and compiles it. It allows sugar such as `json`, and the compiled result is an unconstrained `{}`.— https://github.com/deepseek-ai/deepseek-harness/discussions/9875
- cordis fiber
- A cordis fiber is the execution unit for a plugin's lifecycle; it calls the plugin's `apply` and captures exceptions thrown inside. Exceptions are routed to `context.logger.error` rather than failing plugin loading.— https://github.com/deepseek-ai/deepseek-harness/discussions/9875