shipnoted-mcp 0.2.0 → 0.3.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/README.md +1 -1
- package/index.mjs +51 -2
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -10,4 +10,4 @@ claude mcp add shipnoted \
|
|
|
10
10
|
-- npx -y shipnoted-mcp
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Tools: `list_releases`, `draft_release`, `publish_release`, `
|
|
13
|
+
Tools: `project_info`, `top_feedback`, `search_feedback`, `segments`, `get_request`, `themes`, `loop_status`, `list_releases`, `draft_release`, `set_feedback_status`, `file_request`, `publish_release`, `publish_draft`, `update_release`, `comment_on_request`. Read-only tools are marked `readOnlyHint`, and the ones customers can see have `destructiveHint`, so clients can ask before running them. Needs a Pro or Team plan (API keys). Give the key only the scopes the agent needs (a Read only key makes every write tool fail). Docs: https://shipnoted.com/docs/mcp
|
package/index.mjs
CHANGED
|
@@ -106,17 +106,66 @@ const TOOLS = [
|
|
|
106
106
|
return `Moved ${id} to ${status}.`
|
|
107
107
|
},
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: "project_info",
|
|
111
|
+
description: "Which project this is: name, public address, the widget script tag, and counts of releases, requests and subscribers. A good first call to check the connection.",
|
|
112
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
113
|
+
run: async () => {
|
|
114
|
+
const { project: p } = await call("GET", "/project")
|
|
115
|
+
return `${p.name} (${p.slug}), ${p.live ? "live" : "offline"}\n${p.url}\n${p.counts.releases} releases, ${p.counts.requests} requests, ${p.counts.subscribers} subscribers\nWidget: ${p.widget}`
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "search_feedback",
|
|
120
|
+
description: "Search requests by words or by customer group, for example everything the beta customers asked for. Returns the reference, votes, revenue and status of each.",
|
|
121
|
+
inputSchema: { type: "object", properties: { q: { type: "string" }, segment: { type: "string", description: 'A group such as "plan:Pro", "company:Acme" or "beta:true". See the segments tool.' }, status: { type: "string", enum: ["open", "planned", "progress", "shipped", "declined"] }, sort: { type: "string", enum: ["top", "revenue", "new"] } }, additionalProperties: false },
|
|
122
|
+
run: async ({ q, segment, status, sort }) => {
|
|
123
|
+
const p = new URLSearchParams()
|
|
124
|
+
for (const [k, v] of Object.entries({ q, segment, status, sort })) if (v) p.set(k, String(v))
|
|
125
|
+
const r = await call("GET", `/feedback${p.size ? `?${p}` : ""}`)
|
|
126
|
+
return r.feedback.slice(0, 25).map((f) => `${(f.ref || "").padEnd(7)} ${String(f.votes).padStart(4)} votes${f.mrrCents ? ` $${(f.mrrCents / 100).toLocaleString()}/mo` : ""} [${f.status}] ${f.title}`).join("\n") || "Nothing matches."
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "segments",
|
|
131
|
+
description: "The customer groups that have voted (plan, company, custom attributes like beta:true) with their votes and revenue. Use the segment values with search_feedback.",
|
|
132
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
133
|
+
run: async () => (await call("GET", "/segments")).segments.map((s) => `${s.segment} ${s.votes} votes${s.mrrCents ? `, $${(s.mrrCents / 100).toLocaleString()}/mo` : ""}`).join("\n") || "No identified customers have voted yet.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "file_request",
|
|
137
|
+
description: "File a new feedback request on the board, for example from a support conversation. Send a stable idempotency_key (a ticket id) so a retry never files it twice.",
|
|
138
|
+
inputSchema: { type: "object", properties: { title: { type: "string" }, body: { type: "string" }, email: { type: "string" }, name: { type: "string" }, source: { type: "string", description: "Where it came from, such as Intercom" }, idempotency_key: { type: "string" } }, required: ["title"], additionalProperties: false },
|
|
139
|
+
run: async ({ title, body, email, name, source, idempotency_key }) => {
|
|
140
|
+
const r = await call("POST", "/feedback", { title, body, email, name, source, idempotencyKey: idempotency_key })
|
|
141
|
+
return r.duplicate ? `Already filed as ${r.ref}.` : `Filed as ${r.ref}.`
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "update_release",
|
|
146
|
+
description: "Edit a release (draft or published): title, version, notes, tags, or who it is for. Changes to a published release go live at once.",
|
|
147
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, version: { type: "string" }, body: { type: "string", description: "Markdown" }, tags: { type: "array", items: { type: "string", enum: ["new", "improved", "fixed", "breaking"] } }, audience: { type: "string", description: 'Aim it at a group, such as "plan:pro", or "" for everyone' } }, required: ["id"], additionalProperties: false },
|
|
148
|
+
run: async ({ id, ...fields }) => {
|
|
149
|
+
const r = await call("PATCH", `/releases/${encodeURIComponent(id)}`, fields)
|
|
150
|
+
return `Updated ${r.release.title} (${r.release.status}).`
|
|
151
|
+
},
|
|
152
|
+
},
|
|
109
153
|
]
|
|
110
154
|
|
|
155
|
+
const READ_ONLY = new Set(["list_releases", "top_feedback", "get_request", "loop_status", "themes", "project_info", "search_feedback", "segments"])
|
|
156
|
+
// Tools that change what customers see or get emailed. Clients can use this to ask before running them.
|
|
157
|
+
const PUBLIC_EFFECT = new Set(["publish_release", "publish_draft", "comment_on_request", "update_release"])
|
|
158
|
+
const annotate = (name) => ({ title: name.replace(/_/g, " "), readOnlyHint: READ_ONLY.has(name), destructiveHint: PUBLIC_EFFECT.has(name), idempotentHint: READ_ONLY.has(name) || name === "set_feedback_status", openWorldHint: false })
|
|
159
|
+
|
|
111
160
|
const send = (msg) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n")
|
|
112
161
|
|
|
113
162
|
async function handle(msg) {
|
|
114
163
|
const { id, method, params } = msg
|
|
115
164
|
if (id === undefined) return // notifications need no reply
|
|
116
165
|
try {
|
|
117
|
-
if (method === "initialize") return send({ id, result: { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: "shipnoted", version: "0.
|
|
166
|
+
if (method === "initialize") return send({ id, result: { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: "shipnoted", version: "0.3.0" } } })
|
|
118
167
|
if (method === "ping") return send({ id, result: {} })
|
|
119
|
-
if (method === "tools/list") return send({ id, result: { tools: TOOLS.map(({ run, ...t }) => t) } })
|
|
168
|
+
if (method === "tools/list") return send({ id, result: { tools: TOOLS.map(({ run, ...t }) => ({ ...t, annotations: annotate(t.name) })) } })
|
|
120
169
|
if (method === "tools/call") {
|
|
121
170
|
const tool = TOOLS.find((t) => t.name === params?.name)
|
|
122
171
|
if (!tool) return send({ id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } })
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipnoted-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for Shipnoted: let your coding agent draft and publish release notes and read your users' feedback.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"shipnoted-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
9
16
|
"license": "MIT"
|
|
10
17
|
}
|