DeepSeek Harness plugin registers zero tools: json schema

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginoutput schemaSCHEMA_TYPEStool registration
In DeepSeek Harness, a plugin mounts, logs nothing, and registers no tools when register() uses output schema json. Use defineTool, pass {}, or surface it.

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":

  1. The plugin appears in the loaded list and the profile reports no load failure. Expected: apply was called.
  2. Call ctx.tools.schemas() or inspect the tool list. Expected: the count is zero, so registration left no tools behind.
  3. The console.warn call count is zero and the interface shows no red error. Expected: the failure was swallowed by the logging layer.
  4. 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":

  1. The tool list in the system prompt is empty. Expected: the model has nothing to call.
  2. The user asks for the corresponding operation. Expected: the model answers in text or switches to another tool.
  3. The plugin detail page still shows it as loaded. Expected: troubleshooting drifts toward permissions or networking.
  4. No exception surfaces. Expected: reinstalling the plugin changes nothing unless the schema changes.
  5. logger.error is 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:

  1. register() first checks whether output.render exists. Expected: the tool is rejected when it is missing.
  2. It then calls assertSupportedJsonSchema(output.schema). Expected: the output schema goes through the whitelist check.
  3. SCHEMA_TYPES lists only object, array, string, number, integer, boolean, and null. Expected: json is absent, so validation raises a violation.
  4. The throw bubbles up to the plugin's apply. Expected: the cordis fiber captures it and passes it to context.logger.error.
  5. 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 {}.

  1. Find the hand-written ctx.tools.register({...}) call. Expected: the tool definition site is identified.
  2. Replace it with a defineTool wrapper, passing the name, description, parameters, and output spec. Expected: the tool is defined through the wrapper.
  3. Keep { type: 'json' } in the output. Expected: defineTool converts it to an unconstrained schema during compilation.
  4. Reload the plugin and call ctx.tools.schemas(). Expected: the tool count is restored and the output no longer triggers a SCHEMA_TYPES violation.
js
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.

  1. Locate the output.schema field. Expected: the output contract is visible.
  2. Remove type: 'json' and use the empty object {}. Expected: the schema becomes an empty object.
  3. If the old form expressed a nested structure, use type: 'object' with properties, or keep {} and narrow it inside render. Expected: the schema either expresses the nested shape or no constraint.
  4. Reload and confirm the ctx.tools.schemas() count. Expected: registration succeeds and the model receives a valid schema with no type constraint.
js
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".

  1. Count rejections on the register() call site. Expected: the number of rejected tools is known.
  2. Print a summary line when plugin startup completes. Expected: startup logs show the count.
  3. Connect that count to the DSH plugin market detail page or the system log panel. Expected: users can see "loaded but zero tools".
  4. Add the available alternative to the thrown message: use {} for unconstrained output. Expected: the error tells the author what to write instead.
  5. 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.

  1. Create a real cordis Context and ToolRuntime in the test. Expected: the test uses the production registration path.
  2. Register the tool through the real defineTool or register() path. Expected: no stub stands between the tool and the runtime.
  3. Assert that the ctx.tools.schemas() count matches the expectation. Expected: a mismatch fails the test.
  4. Build a separate case with { type: 'json' }. Expected: it is either compiled or rejected with a readable error.
  5. 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

A DeepSeek Harness plugin is installed and its status is normal, but no tools appear. Why?

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.

Why does DSH plugin registration fail without a single log line, and console.warn is never called?

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'.

In DeepSeek Harness, why can't a hand-written register() output schema use type: json to mean any JSON?

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 {}.

In DeepSeek Harness, why is adding json to SCHEMA_TYPES not an acceptable fix?

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

Sources