zerno-mcp 0.1.2 → 0.1.5
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/README.md +143 -4
- package/dist/index.js +15 -4
- package/dist/setup.js +29 -5
- package/dist/zerno-client.js +25 -9
- package/package.json +13 -2
- package/server.json +28 -0
package/README.md
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
# zerno-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for ZERNO code-agent integrations.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
ZERNO gives AI coding agents project-scoped product context: backlog tasks,
|
|
6
|
+
stage, execution policy, product memory, checkpoints, and run telemetry.
|
|
7
|
+
|
|
8
|
+
## Hosted server
|
|
9
|
+
|
|
10
|
+
Use the hosted Streamable HTTP endpoint when your client supports remote MCP:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
https://zerno.one/mcp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Authentication uses OAuth with PKCE. The public discovery documents are:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
https://zerno.one/.well-known/mcp
|
|
20
|
+
https://zerno.one/.well-known/oauth-protected-resource/mcp
|
|
21
|
+
https://zerno.one/.well-known/oauth-authorization-server
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The OAuth scope is `mcp`. Tokens are project-scoped and grant only ZERNO MCP
|
|
25
|
+
permissions: read project context/tasks/memory, write task status, plans,
|
|
26
|
+
reports, checkpoints, memory notes, intake tasks, and agent run telemetry.
|
|
27
|
+
They do not grant repository, filesystem, shell, or source-code access.
|
|
28
|
+
|
|
29
|
+
## Local stdio server
|
|
6
30
|
|
|
7
31
|
Recommended setup:
|
|
8
32
|
|
|
9
33
|
```bash
|
|
10
|
-
npx -y zerno-mcp@0.1.
|
|
34
|
+
npx -y zerno-mcp@0.1.5 setup
|
|
11
35
|
```
|
|
12
36
|
|
|
13
37
|
The setup command opens ZERNO in the browser, lets the user choose a project,
|
|
@@ -16,7 +40,7 @@ creates the project-scoped MCP token, and writes local agent configs.
|
|
|
16
40
|
Agent runtime:
|
|
17
41
|
|
|
18
42
|
```bash
|
|
19
|
-
npx -y zerno-mcp@0.1.
|
|
43
|
+
npx -y zerno-mcp@0.1.5
|
|
20
44
|
```
|
|
21
45
|
|
|
22
46
|
Required environment:
|
|
@@ -26,3 +50,118 @@ ZERNO_API_URL=https://zerno.one
|
|
|
26
50
|
ZERNO_API_TOKEN=<project-token>
|
|
27
51
|
ZERNO_PROJECT_ID=<project-id>
|
|
28
52
|
```
|
|
53
|
+
|
|
54
|
+
For local stdio, `ZERNO_PROJECT_ID` may also come from a non-secret repository
|
|
55
|
+
binding file:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"project_id": "<project-id>",
|
|
60
|
+
"workspace_id": "<workspace-id>",
|
|
61
|
+
"origin": "git@github.com:org/repo.git"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Place it at `.zerno/project.json`. Environment variables still win, so CI and
|
|
66
|
+
manual project-scoped setups remain deterministic.
|
|
67
|
+
|
|
68
|
+
## Client examples
|
|
69
|
+
|
|
70
|
+
Codex remote MCP:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
codex mcp add zerno --url https://zerno.one/mcp
|
|
74
|
+
codex mcp login zerno --scopes mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Claude Code remote MCP:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add --transport http zerno https://zerno.one/mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then run `/mcp` inside Claude Code to complete OAuth and confirm that ZERNO
|
|
84
|
+
shows connected with tools available.
|
|
85
|
+
|
|
86
|
+
Claude Desktop local stdio:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"zerno": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "zerno-mcp@0.1.5"],
|
|
94
|
+
"env": {
|
|
95
|
+
"ZERNO_API_URL": "https://zerno.one",
|
|
96
|
+
"ZERNO_API_TOKEN": "<project-token>",
|
|
97
|
+
"ZERNO_PROJECT_ID": "<project-id>"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Cursor local stdio uses the same `mcpServers.zerno` block in `.mcp.json`.
|
|
105
|
+
|
|
106
|
+
OpenCode local stdio:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcp": {
|
|
111
|
+
"zerno": {
|
|
112
|
+
"type": "local",
|
|
113
|
+
"command": ["npx", "-y", "zerno-mcp@0.1.5"],
|
|
114
|
+
"enabled": true,
|
|
115
|
+
"environment": {
|
|
116
|
+
"ZERNO_API_URL": "https://zerno.one",
|
|
117
|
+
"ZERNO_API_TOKEN": "<project-token>",
|
|
118
|
+
"ZERNO_PROJECT_ID": "<project-id>"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Main tools
|
|
126
|
+
|
|
127
|
+
- `zerno_get_project_context`
|
|
128
|
+
- `zerno_list_tasks`
|
|
129
|
+
- `zerno_get_task`
|
|
130
|
+
- `zerno_resume_task`
|
|
131
|
+
- `zerno_checkpoint`
|
|
132
|
+
- `zerno_update_task_status`
|
|
133
|
+
- `zerno_memory_snapshot`
|
|
134
|
+
- `zerno_run_start`
|
|
135
|
+
|
|
136
|
+
## Compatibility notes
|
|
137
|
+
|
|
138
|
+
- Remote transport: Streamable HTTP at `/mcp`.
|
|
139
|
+
- Local transport: stdio via the official MCP TypeScript SDK.
|
|
140
|
+
- Server instructions are returned during initialization so Claude Code Tool
|
|
141
|
+
Search can decide when ZERNO tools are relevant.
|
|
142
|
+
- Tool text output is capped to keep Claude Code and Codex context usage
|
|
143
|
+
predictable. Full structured data remains available in `structuredContent`
|
|
144
|
+
on the hosted HTTP transport.
|
|
145
|
+
- The local stdio runtime does not write logs to stdout.
|
|
146
|
+
|
|
147
|
+
## Verification
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm run build
|
|
151
|
+
npm pack --dry-run
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For the hosted endpoint:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
curl -i https://zerno.one/.well-known/mcp
|
|
158
|
+
curl -i https://zerno.one/.well-known/oauth-protected-resource/mcp
|
|
159
|
+
curl -i -X POST https://zerno.one/mcp \
|
|
160
|
+
-H 'content-type: application/json' \
|
|
161
|
+
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Registry metadata
|
|
165
|
+
|
|
166
|
+
The MCP Registry server name is `io.github.zernozerno/zerno-mcp`.
|
|
167
|
+
Package verification is declared with `mcpName` in `package.json`.
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,12 @@ if (process.argv[2] === "setup") {
|
|
|
22
22
|
process.exit(0);
|
|
23
23
|
}
|
|
24
24
|
const client = await import("./zerno-client.js");
|
|
25
|
-
const
|
|
25
|
+
const SERVER_INSTRUCTIONS = "ZERNO provides project-scoped product context for coding agents: backlog tasks, stage, execution policy, product memory, checkpoints, and run telemetry. Use these tools when the user asks to work from ZERNO tasks, inspect backlog/product context, resume prior agent work, record checkpoints, or report task progress. ZERNO never grants repository, filesystem, shell, or source-code access.";
|
|
26
|
+
const MAX_TEXT_CHARS = 12000;
|
|
27
|
+
const server = new Server({ name: "zerno-mcp", version: "0.1.5" }, {
|
|
28
|
+
capabilities: { tools: {} },
|
|
29
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
30
|
+
});
|
|
26
31
|
function asRecord(value) {
|
|
27
32
|
if (value && typeof value === "object") {
|
|
28
33
|
return value;
|
|
@@ -42,6 +47,12 @@ function asTaskList(value) {
|
|
|
42
47
|
const items = rec.tasks ?? rec.items ?? rec.results;
|
|
43
48
|
return Array.isArray(items) ? items : [];
|
|
44
49
|
}
|
|
50
|
+
function capText(value) {
|
|
51
|
+
if (value.length <= MAX_TEXT_CHARS)
|
|
52
|
+
return value;
|
|
53
|
+
const suffix = `\n\n[truncated: ${value.length - MAX_TEXT_CHARS} chars omitted]`;
|
|
54
|
+
return `${value.slice(0, Math.max(0, MAX_TEXT_CHARS - suffix.length))}${suffix}`;
|
|
55
|
+
}
|
|
45
56
|
function renderForTool(name, args, result) {
|
|
46
57
|
switch (name) {
|
|
47
58
|
case "zerno_get_task":
|
|
@@ -812,7 +823,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
812
823
|
throw new Error(`Unknown tool: ${name}`);
|
|
813
824
|
}
|
|
814
825
|
const rendered = renderForTool(name, (args ?? {}), result);
|
|
815
|
-
const fallbackJson = JSON.stringify(result, null, 2);
|
|
826
|
+
const fallbackJson = capText(JSON.stringify(result, null, 2));
|
|
816
827
|
const content = [];
|
|
817
828
|
if (rendered?.html) {
|
|
818
829
|
content.push({
|
|
@@ -828,10 +839,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
828
839
|
});
|
|
829
840
|
}
|
|
830
841
|
if (rendered?.markdown) {
|
|
831
|
-
content.push({ type: "text", text: rendered.markdown });
|
|
842
|
+
content.push({ type: "text", text: capText(rendered.markdown) });
|
|
832
843
|
}
|
|
833
844
|
content.push({ type: "text", text: fallbackJson });
|
|
834
|
-
return { content };
|
|
845
|
+
return { content, _meta: { "anthropic/maxResultSizeChars": MAX_TEXT_CHARS } };
|
|
835
846
|
}
|
|
836
847
|
catch (err) {
|
|
837
848
|
return {
|
package/dist/setup.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
const DEFAULT_API_URL = "https://zerno.one";
|
|
8
|
-
const PACKAGE_SPEC = "zerno-mcp@0.1.
|
|
8
|
+
const PACKAGE_SPEC = "zerno-mcp@0.1.5";
|
|
9
9
|
const MANAGED_START = "# ZERNO MCP:START";
|
|
10
10
|
const MANAGED_END = "# ZERNO MCP:END";
|
|
11
11
|
function argValue(args, name) {
|
|
@@ -130,10 +130,26 @@ async function oauthConnect(apiUrl) {
|
|
|
130
130
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
131
131
|
body: form.toString(),
|
|
132
132
|
});
|
|
133
|
+
if (!token.access_token) {
|
|
134
|
+
throw new Error("OAuth token response missing access_token");
|
|
135
|
+
}
|
|
136
|
+
if (!token.project_id) {
|
|
137
|
+
throw new Error("OAuth token response missing project_id — backend likely on an old build. Re-run setup against the updated server.");
|
|
138
|
+
}
|
|
133
139
|
return { token: token.access_token, projectId: token.project_id };
|
|
134
140
|
}
|
|
135
141
|
function runtimeEnv(apiUrl, rawToken, projectId) {
|
|
136
|
-
|
|
142
|
+
const env = {
|
|
143
|
+
ZERNO_API_URL: apiUrl,
|
|
144
|
+
ZERNO_API_TOKEN: rawToken,
|
|
145
|
+
ZERNO_PROJECT_ID: projectId,
|
|
146
|
+
};
|
|
147
|
+
for (const [key, value] of Object.entries(env)) {
|
|
148
|
+
if (!value || typeof value !== "string") {
|
|
149
|
+
throw new Error(`Refusing to write config: env value for ${key} is empty`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return env;
|
|
137
153
|
}
|
|
138
154
|
function writeJson(filePath, value) {
|
|
139
155
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -155,6 +171,9 @@ function readJson(filePath) {
|
|
|
155
171
|
return JSON.parse(stripJsonComments(readFileSync(filePath, "utf8")));
|
|
156
172
|
}
|
|
157
173
|
function quoteToml(value) {
|
|
174
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
175
|
+
throw new Error(`Cannot serialise empty value to TOML string`);
|
|
176
|
+
}
|
|
158
177
|
return JSON.stringify(value);
|
|
159
178
|
}
|
|
160
179
|
function codexConfigPath() {
|
|
@@ -221,17 +240,22 @@ function writeClaudeCode(env) {
|
|
|
221
240
|
export async function runSetup(args) {
|
|
222
241
|
if (args.includes("--help") || args.includes("-h")) {
|
|
223
242
|
console.log([
|
|
224
|
-
"Usage: npx -y zerno-mcp@0.1.
|
|
243
|
+
"Usage: npx -y zerno-mcp@0.1.5 setup [--api-url https://zerno.one]",
|
|
225
244
|
"",
|
|
226
245
|
"Opens the hosted ZERNO OAuth consent screen, lets the user choose a project,",
|
|
227
|
-
"and writes local Codex, Claude Desktop, and OpenCode MCP configs.",
|
|
246
|
+
"and writes local Codex, Claude Desktop, Claude Code, and OpenCode MCP configs.",
|
|
228
247
|
].join("\n"));
|
|
229
248
|
return;
|
|
230
249
|
}
|
|
231
250
|
const apiUrl = argValue(args, "--api-url") || DEFAULT_API_URL;
|
|
232
251
|
const auth = await oauthConnect(apiUrl);
|
|
233
252
|
const env = runtimeEnv(apiUrl, auth.token, auth.projectId);
|
|
234
|
-
const written = [
|
|
253
|
+
const written = [
|
|
254
|
+
["Codex", writeCodex(env)],
|
|
255
|
+
["Claude Desktop", writeClaude(env)],
|
|
256
|
+
["Claude Code", writeClaudeCode(env)],
|
|
257
|
+
["OpenCode", writeOpenCode(env)],
|
|
258
|
+
];
|
|
235
259
|
console.log("\nZERNO MCP is configured.");
|
|
236
260
|
for (const [client, filePath] of written)
|
|
237
261
|
console.log(`- ${client}: ${filePath}`);
|
package/dist/zerno-client.js
CHANGED
|
@@ -21,22 +21,38 @@ function readKey(jsonLike, key) {
|
|
|
21
21
|
const rx = new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`);
|
|
22
22
|
return jsonLike.match(rx)?.[1] ?? "";
|
|
23
23
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const opencodeText = opencodePath ? readFileSync(opencodePath, "utf8") : "";
|
|
28
|
-
const zernoConfigText = zernoConfigPath ? readFileSync(zernoConfigPath, "utf8") : "";
|
|
29
|
-
let zernoConfigProjectId = "";
|
|
24
|
+
function readJsonFile(path) {
|
|
25
|
+
if (!path)
|
|
26
|
+
return {};
|
|
30
27
|
try {
|
|
31
|
-
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
32
30
|
}
|
|
33
31
|
catch {
|
|
34
|
-
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readStringField(obj, ...keys) {
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const value = obj[key];
|
|
38
|
+
if (typeof value === "string" && value.trim())
|
|
39
|
+
return value.trim();
|
|
35
40
|
}
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
function readLocalFallbackConfig() {
|
|
44
|
+
const opencodePath = findUp("opencode.jsonc");
|
|
45
|
+
const zernoConfigPath = findUp(".zerno/config.json");
|
|
46
|
+
const zernoProjectPath = findUp(".zerno/project.json");
|
|
47
|
+
const opencodeText = opencodePath ? readFileSync(opencodePath, "utf8") : "";
|
|
48
|
+
const zernoConfig = readJsonFile(zernoConfigPath);
|
|
49
|
+
const zernoProject = readJsonFile(zernoProjectPath);
|
|
36
50
|
return {
|
|
37
51
|
apiUrl: readKey(opencodeText, "ZERNO_API_URL"),
|
|
38
52
|
apiToken: readKey(opencodeText, "ZERNO_API_TOKEN"),
|
|
39
|
-
projectId: readKey(opencodeText, "ZERNO_PROJECT_ID") ||
|
|
53
|
+
projectId: readKey(opencodeText, "ZERNO_PROJECT_ID") ||
|
|
54
|
+
readStringField(zernoProject, "project_id", "projectId") ||
|
|
55
|
+
readStringField(zernoConfig, "project_id", "projectId"),
|
|
40
56
|
connectionId: readKey(opencodeText, "ZERNO_CONNECTION_ID"),
|
|
41
57
|
};
|
|
42
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zerno-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"mcpName": "io.github.zernozerno/zerno-mcp",
|
|
4
5
|
"description": "MCP server connecting OpenCode to ZERNO cloud product brain",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
@@ -9,8 +10,18 @@
|
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
12
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"server.json"
|
|
13
15
|
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/zernozerno/betboard-console.git",
|
|
19
|
+
"directory": "packages/zerno-mcp"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://zerno.one",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/zernozerno/betboard-console/issues"
|
|
24
|
+
},
|
|
14
25
|
"publishConfig": {
|
|
15
26
|
"access": "public"
|
|
16
27
|
},
|
package/server.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.zernozerno/zerno-mcp",
|
|
4
|
+
"title": "ZERNO",
|
|
5
|
+
"description": "Project backlog, memory, and execution context for AI coding agents.",
|
|
6
|
+
"version": "0.1.5",
|
|
7
|
+
"websiteUrl": "https://zerno.one",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/zernozerno/betboard-console",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"remotes": [
|
|
13
|
+
{
|
|
14
|
+
"type": "streamable-http",
|
|
15
|
+
"url": "https://zerno.one/mcp"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"packages": [
|
|
19
|
+
{
|
|
20
|
+
"registryType": "npm",
|
|
21
|
+
"identifier": "zerno-mcp",
|
|
22
|
+
"version": "0.1.5",
|
|
23
|
+
"transport": {
|
|
24
|
+
"type": "stdio"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|