@plannotator/artifact-server-claude-channel 0.1.1
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 +134 -0
- package/bin/claude-channel.js +5 -0
- package/index.ts +238 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 backnotprop
|
|
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
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Claude Code live feedback
|
|
2
|
+
|
|
3
|
+
Connect a Claude Code session to Artifact Server through
|
|
4
|
+
[Claude Code Channels](https://code.claude.com/docs/en/channels-reference).
|
|
5
|
+
Reviewers can send open comment threads to the session. Claude receives the
|
|
6
|
+
threads, completes the work, replies, and resolves them in Artifact Server.
|
|
7
|
+
|
|
8
|
+
## Current support
|
|
9
|
+
|
|
10
|
+
Claude Code Channels are a research preview. Custom channels require a
|
|
11
|
+
development flag. Team and Enterprise administrators must also enable the
|
|
12
|
+
`channelsEnabled` organization policy.
|
|
13
|
+
|
|
14
|
+
The Artifact Server channel is currently available from a source checkout.
|
|
15
|
+
The npm package is not published yet.
|
|
16
|
+
|
|
17
|
+
## Before you start
|
|
18
|
+
|
|
19
|
+
Before you start, make sure that:
|
|
20
|
+
|
|
21
|
+
- Artifact Server is running.
|
|
22
|
+
- The Artifact Server source is available locally.
|
|
23
|
+
- Node.js 24.12.0 or later is installed.
|
|
24
|
+
- pnpm 10.34.3 is installed.
|
|
25
|
+
- Claude Code supports Channels.
|
|
26
|
+
|
|
27
|
+
From the Artifact Server source directory, install the dependencies:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Connect to Artifact Server
|
|
34
|
+
|
|
35
|
+
### Local installation
|
|
36
|
+
|
|
37
|
+
If `artifactserver start` runs the server, the channel reads the connection
|
|
38
|
+
from these files:
|
|
39
|
+
|
|
40
|
+
- `~/.artifact-server/local-service.json`
|
|
41
|
+
- `~/.artifact-server/local-api-token`
|
|
42
|
+
|
|
43
|
+
No additional connection settings are necessary.
|
|
44
|
+
|
|
45
|
+
If `pnpm dev` runs the server from source, set the origin and token in the
|
|
46
|
+
shell that starts Claude Code:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export ARTIFACT_SERVER_ORIGIN="http://127.0.0.1:8787"
|
|
50
|
+
export ARTIFACT_SERVER_AGENT_TOKEN="$(<.artifact-server/local-api-token)"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run these commands from the Artifact Server source directory. Do not print or
|
|
54
|
+
commit the token.
|
|
55
|
+
|
|
56
|
+
### Team installation
|
|
57
|
+
|
|
58
|
+
Ask an Artifact Server administrator to issue an API key with these
|
|
59
|
+
permissions:
|
|
60
|
+
|
|
61
|
+
- **Connect agents**
|
|
62
|
+
- **Manage comments**
|
|
63
|
+
|
|
64
|
+
Set the server origin and API key in the shell that starts Claude Code:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export ARTIFACT_SERVER_ORIGIN="https://artifacts.example.com"
|
|
68
|
+
export ARTIFACT_SERVER_AGENT_TOKEN="replace-with-the-api-key"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Do not add the API key to `.mcp.json` or source control.
|
|
72
|
+
|
|
73
|
+
You can also set `ARTIFACT_SERVER_AGENT_NAME` to change the session name that
|
|
74
|
+
appears in Artifact Server. The default name is the current directory name.
|
|
75
|
+
|
|
76
|
+
## Add the channel to Claude Code
|
|
77
|
+
|
|
78
|
+
Add this entry to `.mcp.json` in the project where you use Claude Code:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"artifact-server": {
|
|
84
|
+
"command": "node",
|
|
85
|
+
"args": [
|
|
86
|
+
"/absolute/path/to/artifact-server/integrations/claude-channel/bin/claude-channel.js"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Replace the example path with the absolute path to your Artifact Server
|
|
94
|
+
checkout.
|
|
95
|
+
|
|
96
|
+
## Start Claude Code
|
|
97
|
+
|
|
98
|
+
Start Claude Code from the project that contains `.mcp.json`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
claude --dangerously-load-development-channels server:artifact-server
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Claude Code shows a warning for the development channel. Select **I am using
|
|
105
|
+
this for local development**. If Claude Code also asks whether to use the MCP
|
|
106
|
+
server, select **Use this MCP server**.
|
|
107
|
+
|
|
108
|
+
## Send comments to Claude
|
|
109
|
+
|
|
110
|
+
1. Open an artifact in Artifact Server.
|
|
111
|
+
2. Add one or more comments.
|
|
112
|
+
3. Select the Claude session in the agent picker.
|
|
113
|
+
4. Send the open comments to the session.
|
|
114
|
+
|
|
115
|
+
Claude receives the comments as follow-up work. The channel gives Claude the
|
|
116
|
+
`artifact_comments` tool to read, reply to, and resolve each thread.
|
|
117
|
+
|
|
118
|
+
## Troubleshooting
|
|
119
|
+
|
|
120
|
+
If the channel does not start, run `/mcp` in Claude Code. Make sure that the
|
|
121
|
+
server entry uses the correct absolute path.
|
|
122
|
+
|
|
123
|
+
If Claude Code reports that an organization policy blocked the channel, ask
|
|
124
|
+
an administrator to enable `channelsEnabled`.
|
|
125
|
+
|
|
126
|
+
If the channel starts but no agent appears in Artifact Server, make sure that
|
|
127
|
+
Artifact Server is running. Then make sure that the connection settings are
|
|
128
|
+
available in the shell that started Claude Code.
|
|
129
|
+
|
|
130
|
+
If `MCP_PROTOCOL_NEGOTIATION=auto` is set, Claude Code can reject the MCP
|
|
131
|
+
version of the channel. Unset the variable. Then restart Claude Code.
|
|
132
|
+
|
|
133
|
+
Claude Code writes channel errors to
|
|
134
|
+
`~/.claude/debug/<session-id>.txt`.
|
package/index.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Artifact Server bridge as a Claude Code channel.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code spawns this process over stdio when the channel is registered
|
|
7
|
+
* and opted in. It runs the same claim loop as the Pi bridge (the long poll
|
|
8
|
+
* is the heartbeat, so presence is real), and each claimed bundle is pushed
|
|
9
|
+
* into the session as a `notifications/claude/channel` event. Claude replies
|
|
10
|
+
* to and resolves each thread through the `artifact_comments` tool this
|
|
11
|
+
* server exposes.
|
|
12
|
+
*
|
|
13
|
+
* Evidence tier: `channel` — the bridge reports `delivered` once the
|
|
14
|
+
* notification is written to the transport, which proves admission to the
|
|
15
|
+
* session, not model processing (spec section 4.3).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {homedir, hostname} from "node:os";
|
|
19
|
+
import process from "node:process";
|
|
20
|
+
|
|
21
|
+
import {Server} from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
|
+
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import {
|
|
24
|
+
CallToolRequestSchema,
|
|
25
|
+
ListToolsRequestSchema,
|
|
26
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
27
|
+
import {z} from "zod";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
ActivityBeacon,
|
|
31
|
+
type BridgeHandle,
|
|
32
|
+
chooseDisplayName,
|
|
33
|
+
type CommentOperations,
|
|
34
|
+
createCommentOperations,
|
|
35
|
+
type EnvironmentConfiguration,
|
|
36
|
+
resolveBridgeCredentials,
|
|
37
|
+
startBridge,
|
|
38
|
+
ThreadLocationCache,
|
|
39
|
+
} from "@plannotator/agent-bridge";
|
|
40
|
+
|
|
41
|
+
const channelName = "artifact-server";
|
|
42
|
+
const channelVersion = "0.1.1";
|
|
43
|
+
|
|
44
|
+
function environmentConfiguration(): EnvironmentConfiguration {
|
|
45
|
+
return {
|
|
46
|
+
agentDisplayName: process.env["ARTIFACT_SERVER_AGENT_NAME"],
|
|
47
|
+
agentToken: process.env["ARTIFACT_SERVER_AGENT_TOKEN"],
|
|
48
|
+
origin: process.env["ARTIFACT_SERVER_ORIGIN"],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const artifactCommentsInputSchema = {
|
|
53
|
+
properties: {
|
|
54
|
+
body: {
|
|
55
|
+
description: "Reply text (reply operation only).",
|
|
56
|
+
type: "string",
|
|
57
|
+
},
|
|
58
|
+
operation: {
|
|
59
|
+
description:
|
|
60
|
+
"get_bundle reads threads with their replies; reply posts one " +
|
|
61
|
+
"reply; resolve closes one thread.",
|
|
62
|
+
enum: ["get_bundle", "reply", "resolve"],
|
|
63
|
+
type: "string",
|
|
64
|
+
},
|
|
65
|
+
threadId: {
|
|
66
|
+
description: "Target thread id (reply and resolve operations).",
|
|
67
|
+
type: "string",
|
|
68
|
+
},
|
|
69
|
+
threadIds: {
|
|
70
|
+
description: "Thread ids to read (get_bundle operation).",
|
|
71
|
+
items: {type: "string"},
|
|
72
|
+
type: "array",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ["operation"],
|
|
76
|
+
type: "object",
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
const artifactCommentsArgumentsSchema = z.object({
|
|
80
|
+
body: z.string().optional(),
|
|
81
|
+
operation: z.enum(["get_bundle", "reply", "resolve"]),
|
|
82
|
+
threadId: z.string().optional(),
|
|
83
|
+
threadIds: z.array(z.string()).optional(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
type ArtifactCommentsArguments = z.infer<typeof artifactCommentsArgumentsSchema>;
|
|
87
|
+
|
|
88
|
+
function textContent(text: string) {
|
|
89
|
+
return {content: [{text, type: "text" as const}]};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function runArtifactComments(
|
|
93
|
+
operations: CommentOperations | null,
|
|
94
|
+
parameters: ArtifactCommentsArguments,
|
|
95
|
+
) {
|
|
96
|
+
if (operations === null) {
|
|
97
|
+
throw new Error("Artifact Server is not configured; the bridge is dormant.");
|
|
98
|
+
}
|
|
99
|
+
if (parameters.operation === "get_bundle") {
|
|
100
|
+
const threadIds = parameters.threadIds ?? [];
|
|
101
|
+
if (threadIds.length === 0) throw new Error("get_bundle requires threadIds.");
|
|
102
|
+
const details = [];
|
|
103
|
+
for (const threadId of threadIds) {
|
|
104
|
+
// eslint-disable-next-line no-await-in-loop
|
|
105
|
+
details.push(await operations.getThread(threadId));
|
|
106
|
+
}
|
|
107
|
+
return textContent(JSON.stringify(details, null, 2));
|
|
108
|
+
}
|
|
109
|
+
const threadId = parameters.threadId ?? "";
|
|
110
|
+
if (threadId === "") throw new Error(`${parameters.operation} requires threadId.`);
|
|
111
|
+
if (parameters.operation === "reply") {
|
|
112
|
+
const body = parameters.body ?? "";
|
|
113
|
+
if (body.trim() === "") throw new Error("reply requires a non-empty body.");
|
|
114
|
+
await operations.reply(threadId, body);
|
|
115
|
+
return textContent(`Replied to ${threadId}.`);
|
|
116
|
+
}
|
|
117
|
+
await operations.resolve(threadId);
|
|
118
|
+
return textContent(`Resolved ${threadId}.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function main(): Promise<void> {
|
|
122
|
+
const mcp = new Server(
|
|
123
|
+
{name: channelName, version: channelVersion},
|
|
124
|
+
{
|
|
125
|
+
capabilities: {
|
|
126
|
+
experimental: {"claude/channel": {}},
|
|
127
|
+
tools: {},
|
|
128
|
+
},
|
|
129
|
+
instructions:
|
|
130
|
+
"Artifact Server review bundles arrive as " +
|
|
131
|
+
'<channel source="artifact-server"> events listing comment threads ' +
|
|
132
|
+
"with their ids. Do the work each thread asks for, then use the " +
|
|
133
|
+
"artifact_comments tool: reply on each thread with what you did, " +
|
|
134
|
+
"then resolve it. get_bundle rereads threads when you need the " +
|
|
135
|
+
"full context.",
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const locations = new ThreadLocationCache();
|
|
140
|
+
const beacon = new ActivityBeacon();
|
|
141
|
+
const environment = environmentConfiguration();
|
|
142
|
+
const credentials = await resolveBridgeCredentials(environment, homedir());
|
|
143
|
+
const comments = credentials === null
|
|
144
|
+
? null
|
|
145
|
+
: createCommentOperations(credentials, fetch, locations, beacon);
|
|
146
|
+
|
|
147
|
+
mcp.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
148
|
+
tools: [{
|
|
149
|
+
description:
|
|
150
|
+
"Read, reply to, and resolve Artifact Server comment threads that " +
|
|
151
|
+
"were sent to this session. Use get_bundle to read threads, reply " +
|
|
152
|
+
"to record what you did on a thread, and resolve to close it when " +
|
|
153
|
+
"done.",
|
|
154
|
+
inputSchema: artifactCommentsInputSchema,
|
|
155
|
+
name: "artifact_comments",
|
|
156
|
+
}],
|
|
157
|
+
}));
|
|
158
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
159
|
+
if (request.params.name !== "artifact_comments") {
|
|
160
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
161
|
+
}
|
|
162
|
+
return runArtifactComments(
|
|
163
|
+
comments,
|
|
164
|
+
artifactCommentsArgumentsSchema.parse(request.params.arguments ?? {}),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const transport = new StdioServerTransport();
|
|
169
|
+
await mcp.connect(transport);
|
|
170
|
+
|
|
171
|
+
let bridge: BridgeHandle | null = null;
|
|
172
|
+
bridge = startBridge({
|
|
173
|
+
agentSessionId: null,
|
|
174
|
+
beacon,
|
|
175
|
+
capabilities: {beacon: true, evidence: "channel"},
|
|
176
|
+
credentials,
|
|
177
|
+
displayName: chooseDisplayName(environment, process.cwd()),
|
|
178
|
+
fetchImplementation: fetch,
|
|
179
|
+
host: {
|
|
180
|
+
// No compaction signal exists on this side of the stdio boundary;
|
|
181
|
+
// Claude Code itself queues channel events while the session is busy.
|
|
182
|
+
isCompacting: () => false,
|
|
183
|
+
notify: (message) => {
|
|
184
|
+
// A channel has no user-facing notice surface; stderr reaches the
|
|
185
|
+
// channel log without entering the protocol stream on stdout.
|
|
186
|
+
process.stderr.write(`${message}\n`);
|
|
187
|
+
},
|
|
188
|
+
sendUserMessage: async (text) => {
|
|
189
|
+
// Resolution proves transport admission, not model processing, which
|
|
190
|
+
// is this tier's evidence bound. A rejection fails this dispatch while
|
|
191
|
+
// leaving the channel available for later work.
|
|
192
|
+
try {
|
|
193
|
+
await mcp.notification({
|
|
194
|
+
method: "notifications/claude/channel",
|
|
195
|
+
params: {
|
|
196
|
+
content: text,
|
|
197
|
+
meta: {channel_kind: "artifact_server_dispatch"},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
process.stderr.write("Channel notification failed.\n");
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
hostname: hostname(),
|
|
207
|
+
kind: "claude",
|
|
208
|
+
locations,
|
|
209
|
+
log: (message) => {
|
|
210
|
+
process.stderr.write(`${message}\n`);
|
|
211
|
+
},
|
|
212
|
+
workingDirectory: process.cwd(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const stop = (): void => {
|
|
216
|
+
const active = bridge;
|
|
217
|
+
bridge = null;
|
|
218
|
+
if (active !== null) {
|
|
219
|
+
void active.stop({disconnect: true}).finally(() => {
|
|
220
|
+
process.exit(0);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// The SDK transport exposes a single onclose property, not an event target.
|
|
225
|
+
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
226
|
+
transport.onclose = stop;
|
|
227
|
+
process.on("SIGINT", stop);
|
|
228
|
+
process.on("SIGTERM", stop);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
void (async () => {
|
|
232
|
+
try {
|
|
233
|
+
await main();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
process.stderr.write(`Channel failed to start: ${String(error)}\n`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plannotator/artifact-server-claude-channel",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Claude Code channel bridge for Artifact Server: pushes annotation bundles into an opted-in Claude Code session and closes them through the comment API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"claude-channel",
|
|
8
|
+
"artifact-server"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"artifact-server-claude-channel": "./bin/claude-channel.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"index.ts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
21
|
+
"@plannotator/agent-bridge": "^0.1.1",
|
|
22
|
+
"tsx": "4.23.12",
|
|
23
|
+
"zod": "4.4.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "7.0.2"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"directory": "integrations/claude-channel",
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/plannotator/artifact-server.git"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|