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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.mjs +51 -2
  3. 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`, `top_feedback`, `set_feedback_status`. Needs a Pro or Team plan (API keys).
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.2.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.2.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": { "shipnoted-mcp": "./index.mjs" },
7
- "files": ["index.mjs", "README.md"],
8
- "engines": { "node": ">=18" },
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
  }