Developing Cordis plugins
A Cordis plugin adds code to the assistant: a tool, an event listener or a policy. This guide takes you from an empty folder to a tool the assistant can call. Read Harness design first if Cordis is new to you, and check choosing an extension point: a skill or an MCP server may do the job with less risk.
You need the app installed and a text editor. You do not need access to any private repository, because the packages a tool plugin uses are public on npm.
Your first plugin
The complete example is in docs/plugin-examples/hello-tool.
package.json
{
"name": "hackathon-hello-tool",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"exports": { ".": "./index.js" },
"peerDependencies": { "@deepseek-ai/dsh-tools": "0.2.1-rc.5" }
}
index.js
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'hello-tool'
export const inject = ['tools']
export function apply(ctx, config = {}) {
ctx.tools.register(defineTool({
name: 'hello_microscope',
description: 'Say hello from a user-written Cordis plugin.',
parameters: { who: { type: 'string', description: 'Who to greet.' } },
output: {
schema: { type: 'json' },
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
},
async execute(args) {
return { greeting: `${config.greeting ?? 'Hello'}, ${args.who ?? 'microscopist'}` }
},
}))
}
The three exports are the whole contract:
| Export | Meaning |
|---|---|
name |
The plugin's name |
inject |
Services needed before apply runs |
apply(ctx, config) |
Runs when the plugin starts |
Peer dependency versions must match the installed Harness. A mismatch is refused at install time.
Install it
- Open the control panel (
Cmd+,), then Overview, and select Stop. Plugins can only be installed while the assistant is stopped. - Open Plugins → Install Plugin.
- Select Choose folder or .tgz and pick your plugin folder. You can also type an npm package name.
- Add configuration as JSON if your plugin takes any, for example
{"greeting": "Guten Tag"}. - Select Install Plugin and confirm the warning.
- On Overview, select Start.
A folder is copied at install time. After changing your code, remove the plugin and install it again.
What a plugin can use
Ask for services by name in inject, then use them on ctx.
| Service | Package | Use |
|---|---|---|
tools |
dsh-tools |
Register tools, restrict, guard |
agents, sessions |
dsh-agent, dsh-session |
Agents and persisted sessions |
skills |
dsh-skill |
The skill catalog |
settings, credentials |
dsh-settings, dsh-credentials |
Configuration and secrets |
logger |
cordis |
Logging, through ctx.logger |
edmoreEvents |
EDMORE core | The EDMORE event hub |
edmoreConversationExtensions |
EDMORE core | Workspace panels |
edmoreCapabilities |
EDMORE core | Feature status |
Harness services are documented in client/node_modules/@deepseek-ai/<package>/README.md. EDMORE packages are private
and not on npm, so you can use their services at run time but cannot import their types. EDMORE services are internal
and may change between versions.
Common patterns
Tool parameters
defineTool validates arguments before execute runs. Types are string, number, integer, boolean, null,
array, object, json and oneOf. Mark needed fields with required: true.
parameters: {
path: { type: 'string', required: true, description: 'Absolute file path' },
limit: { type: 'number' },
},
execute(args, exec) receives exec.signal. Pass it to long operations so cancellation works. Throwing is fine: the
model sees Error: <message> and the turn continues.
Reacting to events
Listeners are removed automatically when the plugin stops.
export const inject = ['tools']
export function apply(ctx) {
ctx.on('tools/pre-execute', async (exec, next) => {
ctx.logger.info('tool %s', exec.name)
return next()
})
}
Enforcing a rule
A guard is final: nothing later can reverse its denial. It runs synchronously, and is removed when the plugin stops.
Return a reason to deny, and nothing to allow. The model receives Error: <reason>. This one blocks stage moves:
export function apply(ctx) {
ctx.tools.guard(exec =>
exec.name === 'mcp__tem_twin__move_stage' ? 'Stage moves are disabled on this computer.' : undefined)
}
Cleaning up
Listeners and services are cleaned up for you. For anything else, return a disposer from ctx.effect:
ctx.effect(() => {
const timer = setInterval(poll, 5000)
return () => clearInterval(timer)
})
Adding a workspace panel
Register with edmoreConversationExtensions. The plugin serves the panel's script, here held in panelSource, and the
workspace loads it from /api/edmore/extensions/<id>/panel.js. The id must be lowercase letters, digits and hyphens.
Follow
plugin-tango/src/conversation.mjs and
static/panel.js.
export const inject = ['edmoreConversationExtensions']
export function apply(ctx) {
ctx.effect(() => ctx.edmoreConversationExtensions.register({
id: 'my-panel',
title: 'My panel',
state: async session => ({ note: 'hello' }),
update: async (session, input) => ({ note: input.note }),
asset: async file => file === 'panel.js' ? { body: panelSource, type: 'text/javascript; charset=utf-8' } : undefined,
}))
}
Publish it
Your plugin is an ordinary npm package. Either publish it to npm, or pack it and share the file:
npm pack
Others install the .tgz the same way, under Choose folder or .tgz.
Tools and Workflows
Plan Judge and Workflow are not in the desktop package, so a plugin tool is available to the assistant directly. In a
source checkout with the Workflow Engine on, workflow steps refer to MCP tools, so a tool registered only with
ctx.tools may not appear in the Workflow editor.
Debugging
| Symptom | Check |
|---|---|
| The tool never appears | A service in inject may be missing. The plugin stays pending; read Overview → Process Logs |
| Install fails with a version error | Match peerDependencies to the installed Harness version |
| Your change has no effect | Remove and reinstall: folders are copied at install |
| The plugin is gone after an app update | The app reinstalls plugins after updates. If a folder has moved, the Plugins page names what failed |
To exercise a plugin without the app, load it into a Cordis context with a stand-in service. Run this where
@deepseek-ai/cordis and @deepseek-ai/dsh-tools can be resolved:
import { Context, Service } from '@deepseek-ai/cordis'
import * as plugin from './index.js'
class Tools extends Service {
registered = []
constructor(ctx) { super(ctx, 'tools') }
register(tool) { this.registered.push(tool); return () => {} }
}
const ctx = new Context()
await ctx.plugin(Tools)
await ctx.plugin(plugin, { greeting: 'Guten Tag' })
Security
A plugin is trusted code. It runs inside the assistant process, where it can read saved credentials, use connected instruments, and act without any approval. The app shows a warning before installing one.
- Install only code you have read or trust.
- Plugins can be installed in local mode only, not on a shared cloud server.
- A plugin can bypass the Instruments selection, so be especially careful on a computer connected to a real microscope.
Reference
dsh-tools: tools in full- Cordis
- Plugin manager
- Feature packages: built-in EDMORE plugins, a different mechanism