@kb1slnlabs/dsh-linear 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +356 -2
- package/THIRD_PARTY_NOTICES.md +50 -0
- package/cordis.patch.yml +19 -0
- package/index.js +1889 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KB1SLN-Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,357 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kb1slnlabs/dsh-linear
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Linear issue-tracking tools for the DeepSeek Harness. A Cordis function/namespace
|
|
4
|
+
plugin (`export { name, inject, apply }`, no default export) that registers
|
|
5
|
+
model-facing tools through `ctx.tools.register`.
|
|
6
|
+
|
|
7
|
+
It exposes **two tool surfaces**:
|
|
8
|
+
|
|
9
|
+
1. **GraphQL tools (`linear_*`)** — 20 hand-written tools over the Linear GraphQL
|
|
10
|
+
API (`https://api.linear.app/graphql`), authenticated with a personal API token.
|
|
11
|
+
2. **MCP parity tools (`linear_mcp_*`)** — a dynamic bridge to Linear's **hosted
|
|
12
|
+
MCP server** (`https://mcp.linear.app/mcp`) over the official Streamable HTTP
|
|
13
|
+
transport. These tool definitions are not written here: they are **discovered at
|
|
14
|
+
runtime** with `tools/list`, so their number and shape are whatever Linear serves.
|
|
15
|
+
|
|
16
|
+
> Status: current release **0.1.0**, published as the public GitHub repository
|
|
17
|
+
> `KB1SLN-Labs/dsh-linear` and on npm as `@kb1slnlabs/dsh-linear`. The offline suites below are mock-tested, **except** `test-loader.mjs`
|
|
18
|
+
> and `test-image-runtime.mjs`, which run against the real host services — the plugin
|
|
19
|
+
> loader plus the real `ToolRuntime`/`SystemPrompt` — rather than stubs; the live checks
|
|
20
|
+
> this repo has run are the introspection guard in `test.js` and a real discovery of the
|
|
21
|
+
> hosted MCP tool set — see [Testing](#testing) for exactly what is and is not proven.
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
### GraphQL tools (`linear_*`) — 20 registrations
|
|
26
|
+
|
|
27
|
+
| Tool | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `linear_issue_list` | List issues (filter by team key, state name, free-text search; `limit` default 20, max 50) |
|
|
30
|
+
| `linear_issue_get` | One issue by id or identifier (e.g. `APT-123`), incl. relations |
|
|
31
|
+
| `linear_issue_create` | Create an issue in a team |
|
|
32
|
+
| `linear_issue_update` | Update title, description, state, assignee, labels, priority |
|
|
33
|
+
| `linear_comment_add` | Add a comment to an issue |
|
|
34
|
+
| `linear_comment_list` | List comments on an issue (newest first) |
|
|
35
|
+
| `linear_relation_add` | Link a related issue (`related` / `blockedBy` / `blocks` / `parent` / `child`) |
|
|
36
|
+
| `linear_team_list` | List teams |
|
|
37
|
+
| `linear_team_get` | One team by id (UUID) or key |
|
|
38
|
+
| `linear_state_list` | List workflow states for a team |
|
|
39
|
+
| `linear_label_list` | List labels for a team |
|
|
40
|
+
| `linear_label_get` | One label by id (UUID) |
|
|
41
|
+
| `linear_label_create` | Create a label for a team |
|
|
42
|
+
| `linear_project_list` | List projects |
|
|
43
|
+
| `linear_project_get` | One project by id (UUID) |
|
|
44
|
+
| `linear_project_create` | Create a project (optional team keys) |
|
|
45
|
+
| `linear_cycle_list` | List cycles for a team |
|
|
46
|
+
| `linear_cycle_get` | One cycle by id (UUID) |
|
|
47
|
+
| `linear_user_list` | List users in the workspace |
|
|
48
|
+
| `linear_user_get` | One user by id (UUID) or email |
|
|
49
|
+
|
|
50
|
+
These are the "legacy" surface in the sense that they predate the MCP bridge and
|
|
51
|
+
are hand-modelled against Linear's GraphQL schema. They remain the default and are
|
|
52
|
+
the only tools that work with no MCP token configured.
|
|
53
|
+
|
|
54
|
+
### MCP parity tools (`linear_mcp_*`) — discovered, not declared
|
|
55
|
+
|
|
56
|
+
There is **no fixed list** of these tools in this repo. On connect the bridge pages
|
|
57
|
+
through `tools/list` and registers one harness tool per discovered MCP tool:
|
|
58
|
+
|
|
59
|
+
- public name = `linear_mcp_<rawName>` when it fits the function-name contract
|
|
60
|
+
(≤64 chars, `[A-Za-z0-9_-]`); otherwise a lossy normalization plus a 12-hex
|
|
61
|
+
SHA-256 hash of the identity, so distinct MCP tools never collapse onto one name.
|
|
62
|
+
Names are a pure function of the discovered raw name, so they are stable across
|
|
63
|
+
reconnects.
|
|
64
|
+
- calls are forwarded with `tools/call` using the **raw wire name**, never the public name.
|
|
65
|
+
- the discovered `inputSchema` is passed through unchanged; an advertised
|
|
66
|
+
`outputSchema` is kept only if the harness supports that JSON Schema vocabulary,
|
|
67
|
+
otherwise the output is unconstrained.
|
|
68
|
+
- `notifications/tools/list_changed` triggers re-discovery and an atomic generation
|
|
69
|
+
swap (the full new list is fetched first; only then are the old registrations disposed).
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
Two independent credentials, resolved by the plugin at apply time:
|
|
74
|
+
|
|
75
|
+
| Setting | Env var | Plugin config | Used for | Header sent |
|
|
76
|
+
|---|---|---|---|---|
|
|
77
|
+
| GraphQL key | `LINEAR_API_KEY` | `apiKey` | `linear_*` tools | `Authorization: <raw key>` — **not** `Bearer` |
|
|
78
|
+
| MCP bearer token | `LINEAR_MCP_TOKEN` | `mcp.token` | `linear_mcp_*` bridge | `Authorization: Bearer <token>` |
|
|
79
|
+
|
|
80
|
+
The MCP token falls back in this order: `mcp.token` → `LINEAR_MCP_TOKEN` →
|
|
81
|
+
`apiKey` → `LINEAR_API_KEY`. So a single Linear API key can drive both surfaces.
|
|
82
|
+
The token is placed **only** in the `Authorization` header of requests to `mcp.url`;
|
|
83
|
+
it never appears in a tool name, description, schema, or log line (error text is
|
|
84
|
+
passed through a secret-redaction guard first).
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
# PowerShell (current session)
|
|
88
|
+
$env:LINEAR_API_KEY = "li_api_..."
|
|
89
|
+
$env:LINEAR_MCP_TOKEN = "..." # optional; falls back to LINEAR_API_KEY
|
|
90
|
+
|
|
91
|
+
# or persist for the user
|
|
92
|
+
setx LINEAR_API_KEY "li_api_..."
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `dsh` launcher inherits the parent environment, so any harness started after a
|
|
96
|
+
variable is set will see it.
|
|
97
|
+
|
|
98
|
+
### MCP bridge options (plugin config)
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
# in a profile's cordis.patch.yml
|
|
102
|
+
- insert:
|
|
103
|
+
- id: linear
|
|
104
|
+
name: '@kb1slnlabs/dsh-linear' # or a local path — see Install
|
|
105
|
+
config:
|
|
106
|
+
apiKey: li_api_... # optional, falls back to LINEAR_API_KEY
|
|
107
|
+
mcp:
|
|
108
|
+
token: '' # optional; env fallbacks as above
|
|
109
|
+
url: https://mcp.linear.app/mcp
|
|
110
|
+
toolCallTimeoutMs: 60000 # per-call timeout
|
|
111
|
+
reconnect:
|
|
112
|
+
enabled: true
|
|
113
|
+
initialDelayMs: 500 # bounded exponential backoff
|
|
114
|
+
maxDelayMs: 30000
|
|
115
|
+
maxAttempts: 10
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The repo's own `cordis.patch.yml` inserts the plugin with **no config at all** — it
|
|
119
|
+
relies on the environment variables.
|
|
120
|
+
|
|
121
|
+
`LINEAR_MCP_DISABLED=1` turns the bridge off entirely. The bridge is **fail-soft**:
|
|
122
|
+
with no token, an unloadable MCP SDK, or a host context that lacks `ctx.effect`, the
|
|
123
|
+
GraphQL suite still registers and the bridge simply stays off (a warning is logged).
|
|
124
|
+
After `maxAttempts` consecutive failures the MCP tools are unregistered until the
|
|
125
|
+
plugin reloads or the host restarts.
|
|
126
|
+
|
|
127
|
+
Override env vars used to pin dependency resolution (mainly for offline tests):
|
|
128
|
+
`DSH_LINEAR_TOOLS`, `DSH_LINEAR_SCHEMA`, `DSH_LINEAR_MCP_SDK`,
|
|
129
|
+
`DSH_LINEAR_MCP_SDK_TRANSPORT`, `DSH_LINEAR_MCP_SDK_TYPES`, `DSH_LINEAR_MCP_SDK_ZOD`.
|
|
130
|
+
|
|
131
|
+
### What MCP result content becomes model-visible
|
|
132
|
+
|
|
133
|
+
The harness model vocabulary carries images as **durable attachment references**, not
|
|
134
|
+
inline bytes, so a `linear_mcp_*` result is projected before the model sees it:
|
|
135
|
+
|
|
136
|
+
- An `image` block is admitted **only when the current model route declares image
|
|
137
|
+
input** — positive proof via the host's `llm.resolveModelInfo` for the exact
|
|
138
|
+
provider/model that will read this result, plus a mounted `attachments` service. The
|
|
139
|
+
bytes then go through the host's own admission pipeline (`attachments.saveImages`),
|
|
140
|
+
which owns the count / aggregate-byte / media-type limits.
|
|
141
|
+
- Anything else — a model that does not declare image input, no mounted store, an
|
|
142
|
+
unresolvable route, an invalid block (non-canonical base64, or a media type outside
|
|
143
|
+
PNG/JPEG/WebP/GIF), or an admission rejection (`TOO_MANY_IMAGES`, `IMAGE_TOO_LARGE`,
|
|
144
|
+
…) — yields a text diagnostic in place of that image:
|
|
145
|
+
`[image unavailable: <media type>; <reason>; raw image data remains available to
|
|
146
|
+
programmatic callers]`. One invalid block falls back every image in the same result.
|
|
147
|
+
- **The canonical raw result is never rewritten.** Base64 stays in `result.value` for
|
|
148
|
+
programmatic callers either way; only the model-visible content carries references or
|
|
149
|
+
diagnostics. Text and error behaviour are unchanged.
|
|
150
|
+
- `audio`, embedded `resource`, and `resource_link` blocks stay **text placeholders** —
|
|
151
|
+
they are not admitted as attachments (`resource_link` becomes
|
|
152
|
+
`Resource link: <name> (<uri>)`).
|
|
153
|
+
|
|
154
|
+
## Install
|
|
155
|
+
|
|
156
|
+
Release **0.1.0** is published on the public GitHub repository
|
|
157
|
+
`KB1SLN-Labs/dsh-linear` and on npm as `@kb1slnlabs/dsh-linear`. Four install routes:
|
|
158
|
+
|
|
159
|
+
```powershell
|
|
160
|
+
# 0. install this plugin's own dependencies (local checkout route only)
|
|
161
|
+
npm install
|
|
162
|
+
|
|
163
|
+
# 1. local link with an absolute path (working checkout, edits picked up in place)
|
|
164
|
+
dsh plugin --profile web add link:<absolute-path-to-this-directory>
|
|
165
|
+
|
|
166
|
+
# 2. from the public GitHub repository (default branch — unpinned)
|
|
167
|
+
dsh plugin --profile web add github:KB1SLN-Labs/dsh-linear
|
|
168
|
+
|
|
169
|
+
# 3. from npm, by package name (latest published version)
|
|
170
|
+
dsh plugin --profile web add @kb1slnlabs/dsh-linear
|
|
171
|
+
|
|
172
|
+
# 4. version-pinned release asset — download the tarball attached to the v0.1.0
|
|
173
|
+
# GitHub release, then install it from disk:
|
|
174
|
+
# https://github.com/KB1SLN-Labs/dsh-linear/releases/download/v0.1.0/kb1slnlabs-dsh-linear.tgz
|
|
175
|
+
dsh plugin --profile web add file:./kb1slnlabs-dsh-linear.tgz
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Route 1 (`link:`) points pnpm at the directory rather than copying it, so the profile
|
|
179
|
+
picks up edits in place. Use an **absolute** path — that is what this repo's own install
|
|
180
|
+
used, and it is what makes the spec unambiguous regardless of where `dsh plugin` runs
|
|
181
|
+
from. Routes 2–4 install a copy instead, so later source edits do not leak into the
|
|
182
|
+
profile. Only route 4 pins an exact version: `kb1slnlabs-dsh-linear.tgz` is the release
|
|
183
|
+
asset attached to the v0.1.0 GitHub release — a fixed name renamed from the output of
|
|
184
|
+
`npm pack`, not the filename npm itself generates. Route 2 follows the repository's
|
|
185
|
+
default branch and route 3 follows npm's `latest`, so neither is guaranteed to install
|
|
186
|
+
0.1.0 once newer work or releases exist; use route 4 when reproducibility matters.
|
|
187
|
+
|
|
188
|
+
The clean scratch profile behind the verification described under
|
|
189
|
+
[Testing](#testing) was installed through the **DSH plugin manager**, not by hand.
|
|
190
|
+
Adding the package also adds `@kb1slnlabs/dsh-linear` to the
|
|
191
|
+
profile's `dsh.profile.bundles` layer list, because its manifest declares
|
|
192
|
+
`dsh.bundle.patch`.
|
|
193
|
+
|
|
194
|
+
To add it as a plain dependency instead of a link, use `file:<absolute-path>` or a
|
|
195
|
+
packaged tarball (`npm pack`, then `dsh plugin --profile web add file:<path-to-tgz>`) —
|
|
196
|
+
that is how the scratch profile used by [Testing](#testing) was built.
|
|
197
|
+
|
|
198
|
+
Peer dependencies `@deepseek-ai/dsh-tools` and `@deepseek-ai/cordis`, plus
|
|
199
|
+
`@modelcontextprotocol/sdk` and `zod`, normally resolve from the **dsh install's own**
|
|
200
|
+
`node_modules`. The plugin resolves each dependency as a bare specifier first, then
|
|
201
|
+
from an anchor derived portably from `process.execPath` (the npm/pnpm global prefix),
|
|
202
|
+
then `%APPDATA%/npm/node_modules` on Windows or `/usr/local/lib/node_modules` /
|
|
203
|
+
`/usr/lib/node_modules` on POSIX. The MCP SDK and its zod must come from one and the
|
|
204
|
+
same tree — a partial tree pins nothing.
|
|
205
|
+
|
|
206
|
+
The `@deepseek-ai/dsh-tools` this package pins (`0.1.5-rc.2`) was verified against host
|
|
207
|
+
**0.2.1-rc.3**. In a base-only scratch profile it can log **missing-peer warnings** —
|
|
208
|
+
that is expected: the host supplies those services, and the pinned copy exists so the
|
|
209
|
+
plugin resolves its own peer at all. Do not read such a warning as a failure.
|
|
210
|
+
|
|
211
|
+
## Testing
|
|
212
|
+
|
|
213
|
+
Four suites: the mock halves are **offline** (loopback mock servers only; no real
|
|
214
|
+
credential), but `test.js` additionally runs a live schema check against
|
|
215
|
+
`api.linear.app` whenever `LINEAR_API_KEY` is set — see below, and note that
|
|
216
|
+
`test-loader.mjs` and `test-image-runtime.mjs` are not mocks at all.
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
node test.js # GraphQL half: module surface, 20 tool registrations,
|
|
220
|
+
# 40 GraphQL calls against a mock that mirrors the real
|
|
221
|
+
# Linear schema (flat UUID inputs, plural-field resolution,
|
|
222
|
+
# auth header), plus negative cases; and, when
|
|
223
|
+
# LINEAR_API_KEY is set, the live introspection guard
|
|
224
|
+
node test-mcp-bridge.js # MCP half: naming contract, paginated discovery, Bearer
|
|
225
|
+
# auth with no token leak, raw-name forwarding, isError
|
|
226
|
+
# handling, list_changed re-sync, disposal, fail-soft paths,
|
|
227
|
+
# and image-result projection/admission through the bridge
|
|
228
|
+
node test-loader.mjs # loader half — needs two environment variables, see below
|
|
229
|
+
node test-image-runtime.mjs # image half: real ToolRuntime + SystemPrompt, a stubbed
|
|
230
|
+
# llm route that declares image input and a stubbed
|
|
231
|
+
# attachment store; asserts an MCP image result comes back
|
|
232
|
+
# as a model-visible image block while the raw base64 stays
|
|
233
|
+
# in result.value. No network, no profile, no env vars.
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `test-loader.mjs` — what it runs and what it needs
|
|
237
|
+
|
|
238
|
+
Unlike the mock suites, `test-loader.mjs` exercises the **real plugin loader and the real
|
|
239
|
+
host tool runtime**: it boots a Cordis `Context`, attaches the host's real
|
|
240
|
+
`SystemPrompt` and `ToolRuntime` services (imported from the dsh installation, not
|
|
241
|
+
stubbed), runs `cordis-plugin-loader`, installs this plugin into that context through
|
|
242
|
+
the profile, asserts the entry got a live fiber and that exactly the 20 `linear_*`
|
|
243
|
+
names are known to the runtime — then disposes the fiber and asserts all 20 are gone.
|
|
244
|
+
It carries a 15-second step deadline: before each phase it checks whether 15 seconds
|
|
245
|
+
have elapsed and, if so, fails with `15 second bound exceeded at: <label>`. That
|
|
246
|
+
deadline is a progress budget between steps, **not** a hard timeout — it cannot
|
|
247
|
+
interrupt an asynchronous call that never settles, so a hung import or await stalls the
|
|
248
|
+
run instead of being cut off at 15 seconds.
|
|
249
|
+
|
|
250
|
+
Both variables are **required**; a missing or malformed one fails immediately with an
|
|
251
|
+
actionable message rather than an import error:
|
|
252
|
+
|
|
253
|
+
| Variable | Meaning | Accepted form |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `DSH_ROOT` | the dsh installation directory — the one holding `node_modules/@deepseek-ai/cordis`, from which `cordis`, `cordis-plugin-loader`, `dsh-system-prompt` and `dsh-tools` are resolved | absolute filesystem path (a trailing slash is added for you) |
|
|
256
|
+
| `LINEAR_TEST_PROFILE` | the profile directory to load the plugin against — it must already have this package installed as a dependency/bundle layer | absolute filesystem path **or** a `file:` URL; converted with `pathToFileURL` plus a trailing slash so it is a directory URL, and checked for existence |
|
|
257
|
+
|
|
258
|
+
A non-`file:` URL (`http:`, `npm:`, …) is rejected. Relative paths are not accepted —
|
|
259
|
+
there is no sensible anchor for them here.
|
|
260
|
+
|
|
261
|
+
```powershell
|
|
262
|
+
$env:DSH_ROOT = "C:/Users/<you>/AppData/Roaming/npm/node_modules/@deepseek-ai/dsh"
|
|
263
|
+
$env:LINEAR_TEST_PROFILE = "<dsh-home>/profiles/<profile>" # or file:///<same path>
|
|
264
|
+
node test-loader.mjs
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The suite deletes `LINEAR_API_KEY`, `LINEAR_MCP_TOKEN`, `LINEAR_APIKEY` and
|
|
268
|
+
`LINEAR_KEY` from its own environment before loading, so the plugin it installs cannot
|
|
269
|
+
pick up a live credential and the bridge stays off. That clearing is scoped to this
|
|
270
|
+
process — other suites behave differently: `test.js` deliberately runs live against
|
|
271
|
+
`api.linear.app` when a real key is present (see below).
|
|
272
|
+
|
|
273
|
+
**What this does not cover:** it is not a full headless boot. No LLM, session log,
|
|
274
|
+
filesystem sandbox, or app entry tree is started — only the loader plus those two host
|
|
275
|
+
services inside a bare `Context`. And it never contacts Linear: no GraphQL call and no
|
|
276
|
+
connection to the hosted MCP server are made, so the discovered `linear_mcp_*` surface
|
|
277
|
+
is not exercised here. The scratch profile it runs against is a throwaway directory,
|
|
278
|
+
not your day-to-day profile.
|
|
279
|
+
|
|
280
|
+
`test.js` additionally carries a **live introspection guard** that runs only when
|
|
281
|
+
`LINEAR_API_KEY` is set: it introspects the real Linear schema and asserts every input
|
|
282
|
+
field and query field the plugin emits actually exists. Without a key it prints
|
|
283
|
+
`skip - introspection guard`. So in an environment with no `LINEAR_API_KEY` the suite
|
|
284
|
+
passes with that guard skipped, but **with a key set, `test.js` does use live network** —
|
|
285
|
+
it sends a real introspection request to `https://api.linear.app/graphql`, and a schema
|
|
286
|
+
mismatch makes the run throw rather than skip.
|
|
287
|
+
|
|
288
|
+
### `test-image-runtime.mjs` — what it runs and what it needs
|
|
289
|
+
|
|
290
|
+
No environment variables and no profile: it builds a bare Cordis `Context`, attaches the
|
|
291
|
+
real `SystemPrompt` and `ToolRuntime` (mode `native`), registers one hand-built MCP
|
|
292
|
+
definition, executes it through `tools.execute`, and asserts the finalized content is
|
|
293
|
+
`[text, image-with-attachment-ref]` while `result.value` still carries the raw base64.
|
|
294
|
+
The `llm` route (`inputModalities: ['image']`) and the attachment store are stubs — so
|
|
295
|
+
this proves the bridge's admission **decision** and projection against the real runtime,
|
|
296
|
+
not a real model's image support or the host's real byte/count limits. It needs the same
|
|
297
|
+
peer resolution as the plugin itself (a dsh installation to import `cordis`,
|
|
298
|
+
`dsh-tools`, and `dsh-system-prompt` from), and it can emit the missing-peer warnings
|
|
299
|
+
described under [Install](#install).
|
|
300
|
+
|
|
301
|
+
### What is verified vs. what is not
|
|
302
|
+
|
|
303
|
+
- **Verified (mock-tested):** what `test.js` and `test-mcp-bridge.js` assert —
|
|
304
|
+
registration, GraphQL query/mutation shape, the auth header forms, identifier
|
|
305
|
+
resolution, error payloads, MCP discovery/naming/forwarding/reconnect/disposal
|
|
306
|
+
lifecycle, and image admission refusals against stubbed host services.
|
|
307
|
+
- **Verified against real host services (not mocks):** `test-loader.mjs` loads this
|
|
308
|
+
package through the real plugin loader into a real `ToolRuntime`, and
|
|
309
|
+
`test-image-runtime.mjs` finalizes an MCP image result through the real
|
|
310
|
+
`ToolRuntime`/`SystemPrompt`. The scratch profile behind that run was installed via
|
|
311
|
+
the **DSH plugin manager** and verified on **desktop runtime 0.2.1-rc.3** with the
|
|
312
|
+
Cordis loader plus `ToolRuntime` — a bare host context, **not** a full desktop app
|
|
313
|
+
boot: no LLM turn, session log, sandbox, or app entry tree was started.
|
|
314
|
+
- **Verified (CLI install smoke, separate from the line above):** a clean scratch
|
|
315
|
+
profile based on the **shipped headless template** took the **packed** plugin through
|
|
316
|
+
`dsh plugin`, and `dsh --profile <scratch> --help` exited **0** on **Windows CLI
|
|
317
|
+
0.1.5-rc.2**. That is an install-and-launch smoke check only: no model turn and no
|
|
318
|
+
full desktop GUI boot were performed, and it says nothing about the loader/`ToolRuntime`
|
|
319
|
+
run above, which was on desktop runtime 0.2.1-rc.3.
|
|
320
|
+
- **Verified against live services:** the hosted MCP bridge has been checked against
|
|
321
|
+
the real `https://mcp.linear.app/mcp` endpoint — a real run through the DSH
|
|
322
|
+
`ToolRuntime`, installing this package from a clean scratch package, discovered
|
|
323
|
+
**68** `linear_mcp_*` tools. The live Linear GraphQL introspection guard in `test.js`
|
|
324
|
+
has likewise run with a real key and passed. So the MCP tool count, names, and schemas
|
|
325
|
+
are what Linear actually served at that time, not guesses.
|
|
326
|
+
- **Not verified:** no run in this repo's check has exercised a real `linear_*` **write**
|
|
327
|
+
end-to-end against `api.linear.app` — the GraphQL half is proven against a mock that
|
|
328
|
+
mirrors the introspected schema (plus the live schema-shape guard above), not against
|
|
329
|
+
real issue mutations. Image admission likewise has **not** been exercised with a real
|
|
330
|
+
model route or a real attachment store — only with declared-modalities and stored-ref
|
|
331
|
+
stand-ins. Nothing here was run on more than that one Windows host/runtime, so treat
|
|
332
|
+
other platforms as untested. Linear's hosted MCP tool set can also change server-side,
|
|
333
|
+
so re-discover it with your own key before relying on the exact count or shapes above.
|
|
334
|
+
|
|
335
|
+
## Notes
|
|
336
|
+
|
|
337
|
+
- All GraphQL tools return JSON text; failures (a resolution error, an API error, a
|
|
338
|
+
network error) come back as `{ "error": "…" }` rather than a thrown stack trace.
|
|
339
|
+
- Issue lookups accept either a UUID or a `TEAM-123` identifier.
|
|
340
|
+
- `linear_issue_update` with `assignee: ""` unassigns.
|
|
341
|
+
- The plugin registers no prompt sections and no client UI; it is purely a tool provider.
|
|
342
|
+
|
|
343
|
+
### How identifiers are resolved (resolve-then-mutate)
|
|
344
|
+
|
|
345
|
+
Linear's GraphQL inputs are **flat UUID fields** — `IssueCreateInput` has no nested
|
|
346
|
+
`team`/`state` objects, and the singular `Query.team` field accepts only a UUID `id`.
|
|
347
|
+
So before any write, the plugin resolves human identifiers to UUIDs through the
|
|
348
|
+
**plural** query fields, which *do* accept filters:
|
|
349
|
+
|
|
350
|
+
- a team key/name → `teams(filter: { key: { eq: … } })` → `teamId`
|
|
351
|
+
- a state name → `workflowStates(filter: { team: { id: { eq: <teamId> } }, name: { eq: … } })` → `stateId`
|
|
352
|
+
|
|
353
|
+
If a team key/name does not resolve to exactly one team, the tool fails with a clear
|
|
354
|
+
`Team '<x>' not found` error instead of sending a malformed mutation. An issue
|
|
355
|
+
identifier is resolved by splitting it into team key + number and querying
|
|
356
|
+
`issues(filter: { team: { key: { eq: … } }, number: { eq: … } })`, since `IssueFilter`
|
|
357
|
+
has no `identifier` field.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This plugin, `@kb1slnlabs/dsh-linear`, is licensed under the MIT license in
|
|
4
|
+
`LICENSE` (Copyright (c) 2026 KB1SLN-Labs). This file records the third-party
|
|
5
|
+
material that informed or is reflected in its implementation.
|
|
6
|
+
|
|
7
|
+
## @deepseek-ai/dsh-mcp-client
|
|
8
|
+
|
|
9
|
+
The MCP parity tools in this plugin (`linear_mcp_*`) are a bridge to Linear's
|
|
10
|
+
hosted MCP server. That bridge is **informed by and includes closely related
|
|
11
|
+
implementation patterns** from `@deepseek-ai/dsh-mcp-client`, the DeepSeek
|
|
12
|
+
Harness MCP client bridge. In particular it mirrors that package's naming
|
|
13
|
+
contract for registering discovered MCP tools as harness tools, its handling of
|
|
14
|
+
advertised output schemas (unsupported vocabulary falls back to
|
|
15
|
+
annotation-only), its raw record result schema, and its use of the official
|
|
16
|
+
Model Context Protocol SDK's Streamable HTTP transport with uncached paginated
|
|
17
|
+
`tools/list` discovery.
|
|
18
|
+
|
|
19
|
+
- Package: `@deepseek-ai/dsh-mcp-client`
|
|
20
|
+
- Version: `0.1.5-rc.2`
|
|
21
|
+
- License: MIT (full text reproduced verbatim below)
|
|
22
|
+
- Copyright: (c) 2026 DeepSeek
|
|
23
|
+
- Repository: https://github.com/deepseek-ai/deepseek-harness/tree/main/packages/mcp/mcp-client
|
|
24
|
+
|
|
25
|
+
The full MIT license text as shipped in that package's `LICENSE` file,
|
|
26
|
+
reproduced verbatim:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2026 DeepSeek
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
|
50
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @kb1slnlabs/dsh-linear — profile patch layer
|
|
2
|
+
#
|
|
3
|
+
# Adds the Linear issue-tracking tool suite to a profile. The plugin registers
|
|
4
|
+
# its tools through ctx.tools.register at apply time; no further composition
|
|
5
|
+
# is needed.
|
|
6
|
+
#
|
|
7
|
+
# Config:
|
|
8
|
+
# apiKey: Linear personal API key (li_api_...). Optional — falls back to the
|
|
9
|
+
# LINEAR_API_KEY environment variable.
|
|
10
|
+
#
|
|
11
|
+
# Example row (in a profile's cordis.patch.yml):
|
|
12
|
+
# - id: linear
|
|
13
|
+
# name: '@kb1slnlabs/dsh-linear'
|
|
14
|
+
# config:
|
|
15
|
+
# apiKey: li_api_...
|
|
16
|
+
|
|
17
|
+
- insert:
|
|
18
|
+
- id: linear
|
|
19
|
+
name: '@kb1slnlabs/dsh-linear'
|