prospectapis-mcp 0.2.3 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ All notable changes to `prospectapis-mcp` are documented here. The format follow
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.5] - 2026-10-20
10
+
11
+ ### Added
12
+
13
+ - Four playbooks, served as MCP resources (resources/list, resources/read) and as one prompt each (prompts/list, prompts/get), so a client can offer them as slash commands: newly funded prospects, pre-call brief, funding alerts to a webhook, and an investor's recent rounds. A prompt hands its playbook over as an embedded resource, so the recipe matches what resources/read serves, and a client that omits the optional `arguments` still gets it. Tests hold every name a playbook uses to the tool catalog.
14
+
15
+ ### Changed
16
+
17
+ - The README's first sentence now names the product URL (https://prospectapis.com) and who it is for, because directory cards and search snippets copy that sentence alone and were linking GitHub only. A test pins both.
18
+ - Added CODE_OF_CONDUCT.md (Contributor Covenant 2.1), which open-source programs ask for.
19
+
20
+ ## [0.2.4] - unreleased
21
+
22
+ ### Fixed
23
+
24
+ - Every tool has a human title ("Search Funding Rounds", "Delete Watchlist"), sent as both the tool's `title` and `annotations.title`; before, the title was the raw tool name.
25
+ - `watchlist_delete` now says `destructiveHint: true` (it removes the watchlist and its delivery history), so a client asks before running it; it said `false`, like every other tool. The other writes stay non-destructive: they only add a research job, a watchlist, a test event or a report.
26
+ - `idempotentHint` is set where it is true: the free reads and `watchlist_delete`. `funding_search` and `funding_get` stay `false`, because every call that returns records is billed again. `openWorldHint` stays `true` on every tool.
27
+ - One factory, `annotationsFor()` in `src/tools.js`, builds these annotations for the npm server and the hosted MCP door alike. `test/annotations.test.js` checks every served tool (a title that is not the raw name and has no underscore, every `*_delete` tool destructive and not read-only) and plants each regression to prove the check fails on it.
28
+
29
+ - README opening rewritten for the listings that copy it word for word (Glama, mcpservers.org, the MCP Registry, npm): one paragraph on what the server does and costs, then the install lines and the website, plus a three-question FAQ. No tool or behaviour change; 14 tools.
30
+ - `package-lock.json` carries the package version again (it still said 0.2.2).
31
+ - `test/readme-opening.test.js` checks the opening against the package name, its Website line against `server.json` `websiteUrl`, every tool count the README states against `src/tools.js`, the stated Node floor against `engines`, and one version across `package.json`, `server.json` and `package-lock.json`.
32
+ - The README no longer says every funding record carries its announcement URL: some records come back without one, so the opening and the FAQ now say records carry the public announcement URL when there is one, and `test/readme-opening.test.js` fails on any sentence that promises a URL on every record.
33
+ - `test/package-scripts.test.js` checks that every file an npm script runs exists in the checkout.
34
+
9
35
  ## [0.2.3] - 2026-10-11
10
36
 
11
37
  ### Added
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # prospectapis-mcp
2
2
 
3
- MCP server for the ProspectAPIs funding data and research API. It gives an MCP client (Claude Desktop, Claude
4
- Code, Cursor and others) fourteen tools over `https://api.prospectapis.com`, one per customer endpoint (the
5
- liveness probe `GET /health` has none; `api_version` answers the same question). Five tools change state:
6
- `research_submit` (starts a paid research job), `watchlist_create`, `watchlist_test`, `watchlist_delete` and
7
- `feedback_send`; every other tool only reads.
3
+ prospectapis-mcp is the MCP server for ProspectAPIs (https://prospectapis.com), for sales and prospecting teams and the AI agents they run in Claude Code, Claude Desktop, Cursor and other MCP clients. It gives your agent fourteen tools over the ProspectAPIs API: search funding rounds by date, round, amount, company, investor and country, get new matching rounds pushed to your webhook, and order a cited research brief on any company or person. Funding records carry the public announcement URL when there is one. Records cost $0.02 each after 100 free a month, a completed brief costs $0.15, and there are no seats, no subscription and no card to sign up. It needs Node.js 22 or later.
4
+
5
+ Install (Claude Code): `claude mcp add prospectapis -e PROSPECTAPIS_API_KEY=pa_live_... -- npx -y prospectapis-mcp`
6
+
7
+ Other clients: run `npx -y prospectapis-mcp` with `PROSPECTAPIS_API_KEY` from https://prospectapis.com
8
+
9
+ Website: https://prospectapis.com/mcp
8
10
 
9
11
  | Tool | Endpoint | Cost |
10
12
  |---|---|---|
@@ -50,6 +52,14 @@ Client config (for example `claude_desktop_config.json`):
50
52
 
51
53
  Claude Code: `claude mcp add prospectapis -e PROSPECTAPIS_API_KEY=pa_live_... -- npx -y prospectapis-mcp`.
52
54
 
55
+ ## FAQ
56
+
57
+ **Where does the funding data come from?** Records carry the public announcement URL when there is one.
58
+
59
+ **What is free?** 100 funding records a month, any search that matches nothing, watchlist management, and the balance, pricing and version tools.
60
+
61
+ **Which clients work?** Claude Code, Claude Desktop, Cursor and any client that reads an mcpServers config, through `npx -y prospectapis-mcp`.
62
+
53
63
  ## Develop
54
64
 
55
65
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prospectapis-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "mcpName": "io.github.ProspectAPIs/prospectapis-mcp",
5
5
  "description": "MCP server for ProspectAPIs: search funding rounds, run paid prospect research briefs, manage funding watchlists, read your balance and the live price list.",
6
6
  "type": "module",
@@ -0,0 +1,113 @@
1
+ // Playbooks: short recipes an agent runs with the tools in src/tools.js, served over the MCP RESOURCE primitive
2
+ // (resources/list, resources/read) and offered as one PROMPT each (prompts/list, prompts/get), so a client can show
3
+ // them as slash commands.
4
+ //
5
+ // WHY RESOURCES AND PROMPTS, NOT TOOLS. Every tool in src/tools.js is one REST endpoint (test/api-parity.test.js
6
+ // holds the catalog to the API). A recipe wraps no endpoint; it is read-only context an agent fetches when it needs
7
+ // it, which is what resources are for, and a prompt is how a client offers that recipe to a user.
8
+ //
9
+ // A DESCRIPTION IS LISTED, A BODY IS FETCHED. Clients show the title and description beside every tool, so those
10
+ // state what the recipe does in plain product terms and never instruct the model. The body is returned only for a
11
+ // URI or prompt the caller asked for by name, so it names tools freely.
12
+ //
13
+ // Every figure in a body (prices, page size, alert limit, poll interval) is the one the tool descriptions in
14
+ // src/tools.js state, checked by review, not by a test. test/playbooks.test.js fails if a body names a tool,
15
+ // parameter, value or response field the catalog does not have.
16
+
17
+ export const PLAYBOOK_MIME = "text/markdown";
18
+
19
+ const NEWLY_FUNDED = `# Newly funded prospects
20
+
21
+ Build a short list of companies that just raised, worth reaching out to while the round is fresh.
22
+
23
+ 1. \`account_balance\` first. Each funding record returned costs $0.02 after 100 free a month, so know what is left
24
+ before you pull pages.
25
+ 2. \`funding_search\` with a tight window and the filters that describe the buyer: \`since\` (for example the last 30
26
+ days), \`round\` (such as \`seed,series_a\`), \`country\`, and \`min_amount_usd\` or \`max_amount_usd\`. Start with
27
+ \`limit\` 10 to 25. A search that matches nothing costs nothing, so narrow before you widen.
28
+ 3. Read the results. Each record has the company, its domain, the round, the amount, the investors and, when there is
29
+ one, the URL of the public announcement. Drop companies without a domain or outside the buyer profile.
30
+ 4. Page with \`cursor\` only if the first page is all good fits; pass \`next_cursor\` back exactly as returned. A null
31
+ \`next_cursor\` means there are no more matches.
32
+ 5. For the two or three best fits, run the pre-call brief playbook (\`research_submit\` with \`domain\`).
33
+ 6. Present the list as company, domain, round, amount, date, lead investors and the announcement URL where there is
34
+ one, so every line can be checked.
35
+ `;
36
+
37
+ const PRE_CALL_BRIEF = `# Pre-call brief
38
+
39
+ A cited brief on one company or person before a call or a first email.
40
+
41
+ 1. \`research_submit\` with exactly ONE identifier: \`domain\` is best; otherwise \`website_url\`, \`company_name\`, a work
42
+ \`email\`, or \`person_name\` together with \`company_name\`. Set \`purpose\` to \`pre_call\` for objections and discovery
43
+ questions, or \`outreach\` for sourced hooks with draft first lines. Add \`context\` (your own product, up to 1,000
44
+ characters) to get a \`fit\` section.
45
+ 2. It returns \`research_id\` at once. The brief takes 20 to 90 seconds: call \`research_get\` with that id every 10 to 15
46
+ seconds until \`status\` is \`completed\` or \`failed\`. Polling is free.
47
+ 3. A brief is charged only when it completes, at the \`research_price_usd\` that \`account_balance\` reports, and refunded
48
+ in full if it fails. Free records do not apply to research.
49
+ 4. Every field in \`result\` is \`{value, sources, confidence}\`. Quote the sources with the claims you use, and say when
50
+ a field came back empty rather than filling the gap.
51
+ `;
52
+
53
+ const FUNDING_ALERTS = `# Funding alerts to a webhook
54
+
55
+ Get every new funding round that matches a profile pushed to your own endpoint, instead of searching again.
56
+
57
+ 1. Decide the filters with \`funding_search\` first (\`round\`, \`country\`, \`min_amount_usd\`, \`max_amount_usd\`,
58
+ \`investor\`, \`company\` or \`domain\`), and check the results look right. Date windows and paging do not apply to an
59
+ alert.
60
+ 2. \`watchlist_create\` with a \`name\`, those \`filters\` and an https \`webhook_url\` on a public host. Save the \`secret\`
61
+ it returns: it is shown once, and every delivery is signed with it in the Prospect-Signature header
62
+ (t=..,v1=HMAC-SHA256). Creating is free; each delivered event costs one record, charged only when your webhook
63
+ answers 2xx. An account can have 20 active alerts.
64
+ 3. \`watchlist_test\` sends a signed sample event now, so the receiver can be built and checked before a real round
65
+ arrives. Free, 5 a minute.
66
+ 4. Later, \`watchlist_deliveries\` shows the last 50 deliveries and their outcome (delivered, retry, failed,
67
+ no_credit), and \`watchlist_get\` shows whether the alert is active and why it was paused.
68
+ `;
69
+
70
+ const INVESTOR_ROUNDS = `# An investor's recent rounds
71
+
72
+ See which companies an investor has backed lately, to find warm introductions or a portfolio to sell into.
73
+
74
+ 1. \`funding_search\` with \`investor\` set to the investor's exact name (case-insensitive) and a \`since\` date, newest
75
+ first. Add \`round\` to keep to one stage.
76
+ 2. Each record lists every investor named and the lead investors, so you can tell a lead from a participation.
77
+ 3. Group the results by round and by country, and give the announcement URL with each line that has one.
78
+ 4. To follow this investor from now on, run the funding alerts playbook with \`investor\` as the filter.
79
+ `;
80
+
81
+ export const PLAYBOOKS = Object.freeze([
82
+ {
83
+ name: "newly-funded-prospects",
84
+ uri: "prospectapis://playbooks/newly-funded-prospects",
85
+ title: "Newly funded prospects",
86
+ description: "Build a short list of companies that raised recently, filtered by round, country and amount, with the announcement for each.",
87
+ text: NEWLY_FUNDED,
88
+ prompt: { arg: "profile", describe: "Who you sell to, for example 'seed and Series A fintech in the US'.", ask: (v) => `Run this playbook for: ${v}` },
89
+ },
90
+ {
91
+ name: "pre-call-brief",
92
+ uri: "prospectapis://playbooks/pre-call-brief",
93
+ title: "Pre-call brief",
94
+ description: "A cited research brief on one company or person before a call or a first email.",
95
+ text: PRE_CALL_BRIEF,
96
+ prompt: { arg: "prospect", describe: "The company domain, company name, or person and company.", ask: (v) => `Run this playbook for: ${v}` },
97
+ },
98
+ {
99
+ name: "funding-alerts",
100
+ uri: "prospectapis://playbooks/funding-alerts",
101
+ title: "Funding alerts to a webhook",
102
+ description: "Push every new funding round that matches a profile to your own https endpoint, signed, instead of searching again.",
103
+ text: FUNDING_ALERTS,
104
+ },
105
+ {
106
+ name: "investor-rounds",
107
+ uri: "prospectapis://playbooks/investor-rounds",
108
+ title: "An investor's recent rounds",
109
+ description: "The companies one investor has backed lately, split into lead and participation, with sources.",
110
+ text: INVESTOR_ROUNDS,
111
+ prompt: { arg: "investor", describe: "The investor's name, for example 'Example Ventures'.", ask: (v) => `Run this playbook for: ${v}` },
112
+ },
113
+ ]);
package/src/server.js CHANGED
@@ -5,7 +5,10 @@
5
5
 
6
6
  import { createRequire } from "node:module";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { TOOLS, buildUrl, buildPath, buildBody } from "./tools.js";
8
+ import { TOOLS, buildUrl, buildPath, buildBody, annotationsFor } from "./tools.js";
9
+ import { z } from "zod";
10
+ import { GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
+ import { PLAYBOOKS, PLAYBOOK_MIME } from "./playbooks.js";
9
12
 
10
13
  export const { version: VERSION } = createRequire(import.meta.url)("../package.json");
11
14
 
@@ -91,13 +94,60 @@ export function createServer({
91
94
  }
92
95
 
93
96
  const server = new McpServer({ name: "prospectapis", version: VERSION }, { instructions: INSTRUCTIONS });
97
+
98
+ // Playbooks (src/playbooks.js): each one a resource (resources/list, resources/read) and a prompt (prompts/list,
99
+ // prompts/get). The prompt hands the playbook over as an embedded resource, so the recipe a user picks is exactly
100
+ // the one resources/read serves. Registering either declares that capability, which a server with none does not
101
+ // advertise.
102
+ for (const p of PLAYBOOKS) {
103
+ server.registerResource(
104
+ p.name,
105
+ p.uri,
106
+ { title: p.title, description: p.description, mimeType: PLAYBOOK_MIME },
107
+ async (uri) => ({ contents: [{ uri: uri.href, mimeType: PLAYBOOK_MIME, text: p.text }] }),
108
+ );
109
+ }
110
+ // `arguments` is optional on prompts/get (MCP spec), but the SDK validates a prompt that declares arguments against
111
+ // a required object, so a client that leaves it out would get -32602 for an argument it was told is optional. The
112
+ // SDK installs its prompts/get handler through the low-level Server the first time a prompt is registered; wrapping
113
+ // setRequestHandler before that defaults a missing `arguments` to {} and leaves every other request untouched.
114
+ {
115
+ const low = server.server;
116
+ const setRequestHandler = low.setRequestHandler.bind(low);
117
+ low.setRequestHandler = (schema, handler) =>
118
+ setRequestHandler(
119
+ schema,
120
+ schema === GetPromptRequestSchema
121
+ ? (request, extra) => handler({ ...request, params: { ...request.params, arguments: request.params?.arguments ?? {} } }, extra)
122
+ : handler,
123
+ );
124
+ }
125
+ for (const p of PLAYBOOKS) {
126
+ const argsSchema = p.prompt ? { [p.prompt.arg]: z.string().max(200).optional().describe(p.prompt.describe) } : undefined;
127
+ server.registerPrompt(
128
+ p.name,
129
+ { title: p.title, description: p.description, ...(argsSchema ? { argsSchema } : {}) },
130
+ async (args = {}) => {
131
+ const v = p.prompt ? String(args[p.prompt.arg] ?? "").trim() : "";
132
+ return {
133
+ description: p.description,
134
+ messages: [
135
+ { role: "user", content: { type: "resource", resource: { uri: p.uri, mimeType: PLAYBOOK_MIME, text: p.text } } },
136
+ { role: "user", content: { type: "text", text: p.prompt && v ? p.prompt.ask(v) : "Run this playbook." } },
137
+ ],
138
+ };
139
+ },
140
+ );
141
+ }
142
+
94
143
  for (const tool of TOOLS) {
95
144
  server.registerTool(
96
145
  tool.name,
97
146
  {
147
+ title: tool.title,
98
148
  description: tool.description,
99
149
  inputSchema: tool.shape,
100
- annotations: { title: tool.name, readOnlyHint: !tool.write, destructiveHint: false, openWorldHint: true },
150
+ annotations: annotationsFor(tool),
101
151
  },
102
152
  (args) => callEndpoint(tool, args),
103
153
  );
package/src/tools.js CHANGED
@@ -10,7 +10,13 @@
10
10
  // - `write: true` marks a tool that changes state or spends credits
11
11
  // (research_submit starts a paid job); the server turns `write` into MCP's
12
12
  // readOnlyHint, so every other tool is read-only;
13
- // - `auth: false` marks the tools the API serves without a key (pricing_get, api_version).
13
+ // - `auth: false` marks the tools the API serves without a key (pricing_get, api_version);
14
+ // - `title` is the human name a client shows instead of the raw tool name;
15
+ // - `idempotent: true` marks a tool whose repeat call with the same arguments changes nothing more: the free
16
+ // reads, and watchlist_delete (a second delete finds nothing to delete). funding_search and funding_get are
17
+ // read-only but NOT idempotent, because every call that returns records is billed again.
18
+ // annotationsFor() below turns these fields into MCP tool annotations; the npm server and the hosted door
19
+ // (apps/api/src/lib/mcp-door.js, through createServer) both register every tool with it, so they cannot drift.
14
20
 
15
21
  import { z } from "zod";
16
22
 
@@ -38,6 +44,7 @@ export const TOOLS = [
38
44
  name: "funding_search",
39
45
  method: "GET",
40
46
  path: "/v1/funding",
47
+ title: "Search Funding Rounds",
41
48
  write: false,
42
49
  format: { min_amount_usd: formatAmount, max_amount_usd: formatAmount },
43
50
  description:
@@ -55,7 +62,7 @@ export const TOOLS = [
55
62
  company: z.string().min(1).max(100).optional().describe("Company name contains this text (case-insensitive)."),
56
63
  domain: z.string().min(3).max(253).optional().describe("Exact company domain, for example `example.com`."),
57
64
  investor: z.string().min(1).max(100).optional().describe("Exact investor name, case-insensitive."),
58
- country: z.string().min(1).max(60).optional().describe("Headquarters country, case-insensitive."),
65
+ country: z.string().min(1).max(60).optional().describe("Headquarters country as a name or ISO code, such as US, United States or DE. Other text, such as a city, matches the stated location."),
59
66
  limit: z.number().int().min(1).max(100).optional().describe("Records per page, 1 to 100 (default 25). Each record returned is billed."),
60
67
  cursor: z.string().max(200).optional().describe("Opaque cursor from the previous page's `next_cursor`."),
61
68
  },
@@ -64,6 +71,7 @@ export const TOOLS = [
64
71
  name: "funding_get",
65
72
  method: "GET",
66
73
  path: "/v1/funding/{id}",
74
+ title: "Get Funding Round",
67
75
  write: false,
68
76
  description: "Read one funding record by id. Costs $0.02 when found (free records are used first); a missing id is free.",
69
77
  shape: {
@@ -74,6 +82,7 @@ export const TOOLS = [
74
82
  name: "research_submit",
75
83
  method: "POST",
76
84
  path: "/v1/research",
85
+ title: "Start Prospect Research",
77
86
  write: true,
78
87
  body: true,
79
88
  description:
@@ -103,6 +112,8 @@ export const TOOLS = [
103
112
  name: "research_get",
104
113
  method: "GET",
105
114
  path: "/v1/research/{id}",
115
+ title: "Get Research Brief",
116
+ idempotent: true,
106
117
  write: false,
107
118
  description:
108
119
  "Read a research job by the `research_id` research_submit returned: `status` (queued, running, completed, failed), `input`, " +
@@ -118,6 +129,7 @@ export const TOOLS = [
118
129
  name: "watchlist_create",
119
130
  method: "POST",
120
131
  path: "/v1/watchlists",
132
+ title: "Create Funding Watchlist",
121
133
  body: true,
122
134
  write: true,
123
135
  description:
@@ -145,6 +157,8 @@ export const TOOLS = [
145
157
  name: "watchlist_list",
146
158
  method: "GET",
147
159
  path: "/v1/watchlists",
160
+ title: "List Watchlists",
161
+ idempotent: true,
148
162
  write: false,
149
163
  description: "Your funding-signal alerts (without their secrets). Free.",
150
164
  shape: {},
@@ -153,6 +167,8 @@ export const TOOLS = [
153
167
  name: "watchlist_get",
154
168
  method: "GET",
155
169
  path: "/v1/watchlists/{id}",
170
+ title: "Get Watchlist",
171
+ idempotent: true,
156
172
  write: false,
157
173
  description: "One funding-signal alert: its name, filters, webhook URL, whether it is active and why it was paused (never its secret). Free.",
158
174
  shape: { id: z.string().uuid().describe("The watchlist id.") },
@@ -161,6 +177,8 @@ export const TOOLS = [
161
177
  name: "watchlist_deliveries",
162
178
  method: "GET",
163
179
  path: "/v1/watchlists/{id}/deliveries",
180
+ title: "List Watchlist Deliveries",
181
+ idempotent: true,
164
182
  write: false,
165
183
  description: "The last 50 deliveries of one alert and their outcome (delivered, retry, failed, no_credit). Free.",
166
184
  shape: { id: z.string().uuid().describe("The watchlist id.") },
@@ -169,6 +187,7 @@ export const TOOLS = [
169
187
  name: "watchlist_test",
170
188
  method: "POST",
171
189
  path: "/v1/watchlists/{id}/test",
190
+ title: "Send Watchlist Test Event",
172
191
  body: true,
173
192
  write: true,
174
193
  description: "Send a signed sample event to the alert's webhook now, to build and verify the receiver. Free; 5 a minute.",
@@ -178,6 +197,8 @@ export const TOOLS = [
178
197
  name: "watchlist_delete",
179
198
  method: "DELETE",
180
199
  path: "/v1/watchlists/{id}",
200
+ title: "Delete Watchlist",
201
+ idempotent: true,
181
202
  write: true,
182
203
  description: "Delete a funding-signal alert and its delivery history. Free.",
183
204
  shape: { id: z.string().uuid().describe("The watchlist id.") },
@@ -186,6 +207,8 @@ export const TOOLS = [
186
207
  name: "account_balance",
187
208
  method: "GET",
188
209
  path: "/v1/account",
210
+ title: "Get Account Balance",
211
+ idempotent: true,
189
212
  write: false,
190
213
  description: "Your credit balance and the free records left this month. Free. Check it before a large paged search.",
191
214
  shape: {},
@@ -194,6 +217,7 @@ export const TOOLS = [
194
217
  name: "feedback_send",
195
218
  method: "POST",
196
219
  path: "/v1/feedback",
220
+ title: "Send Feedback",
197
221
  body: true,
198
222
  write: true,
199
223
  description:
@@ -212,6 +236,8 @@ export const TOOLS = [
212
236
  name: "pricing_get",
213
237
  method: "GET",
214
238
  path: "/pricing",
239
+ title: "Get Pricing",
240
+ idempotent: true,
215
241
  write: false,
216
242
  auth: false,
217
243
  description:
@@ -223,6 +249,8 @@ export const TOOLS = [
223
249
  name: "api_version",
224
250
  method: "GET",
225
251
  path: "/v1/version",
252
+ title: "Get API Version",
253
+ idempotent: true,
226
254
  write: false,
227
255
  auth: false,
228
256
  description: "The API's version and deployed build. Free, needs no key.",
@@ -270,3 +298,17 @@ export function buildBody(tool, args) {
270
298
  }
271
299
  return JSON.stringify(out);
272
300
  }
301
+
302
+ // The one place a catalog entry becomes MCP tool annotations. A tool is destructive when it is a write whose
303
+ // method is DELETE (watchlist_delete removes the watchlist and its delivery history); every other write only adds
304
+ // (a research job, a watchlist, a test event, a report), so it is a write that is not destructive.
305
+ export function annotationsFor(tool) {
306
+ const write = !!tool.write;
307
+ return {
308
+ title: tool.title,
309
+ readOnlyHint: !write,
310
+ destructiveHint: write && tool.method === "DELETE",
311
+ idempotentHint: !!tool.idempotent,
312
+ openWorldHint: true,
313
+ };
314
+ }