@labelgrid/mcp 0.1.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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +299 -0
  4. package/dist/api/content-types.d.ts +33 -0
  5. package/dist/api/content-types.js +87 -0
  6. package/dist/api/http.d.ts +62 -0
  7. package/dist/api/http.js +345 -0
  8. package/dist/api/upload.d.ts +26 -0
  9. package/dist/api/upload.js +104 -0
  10. package/dist/config.d.ts +29 -0
  11. package/dist/config.js +82 -0
  12. package/dist/coverage.d.ts +19 -0
  13. package/dist/coverage.js +150 -0
  14. package/dist/gating.d.ts +13 -0
  15. package/dist/gating.js +22 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.js +86 -0
  18. package/dist/legal.d.ts +12 -0
  19. package/dist/legal.js +13 -0
  20. package/dist/log.d.ts +16 -0
  21. package/dist/log.js +35 -0
  22. package/dist/server.d.ts +15 -0
  23. package/dist/server.js +83 -0
  24. package/dist/tools/accounting.d.ts +12 -0
  25. package/dist/tools/accounting.js +386 -0
  26. package/dist/tools/analytics.d.ts +3 -0
  27. package/dist/tools/analytics.js +62 -0
  28. package/dist/tools/catalog-read.d.ts +10 -0
  29. package/dist/tools/catalog-read.js +145 -0
  30. package/dist/tools/catalog-write.d.ts +12 -0
  31. package/dist/tools/catalog-write.js +206 -0
  32. package/dist/tools/delivery.d.ts +6 -0
  33. package/dist/tools/delivery.js +40 -0
  34. package/dist/tools/files-read.d.ts +7 -0
  35. package/dist/tools/files-read.js +86 -0
  36. package/dist/tools/full-writes.d.ts +12 -0
  37. package/dist/tools/full-writes.js +248 -0
  38. package/dist/tools/identity.d.ts +3 -0
  39. package/dist/tools/identity.js +28 -0
  40. package/dist/tools/reference.d.ts +3 -0
  41. package/dist/tools/reference.js +36 -0
  42. package/dist/tools/release-write.d.ts +12 -0
  43. package/dist/tools/release-write.js +184 -0
  44. package/dist/tools/review-read.d.ts +7 -0
  45. package/dist/tools/review-read.js +78 -0
  46. package/dist/tools/setup.d.ts +11 -0
  47. package/dist/tools/setup.js +56 -0
  48. package/dist/tools/types.d.ts +41 -0
  49. package/dist/tools/types.js +52 -0
  50. package/dist/tools/webhooks.d.ts +7 -0
  51. package/dist/tools/webhooks.js +124 -0
  52. package/dist/version.d.ts +6 -0
  53. package/dist/version.js +17 -0
  54. package/package.json +34 -0
  55. package/server.json +59 -0
@@ -0,0 +1,36 @@
1
+ /** Reference toolset: one tool serving all read-only lookup datasets. */
2
+ import { z } from 'zod';
3
+ /** Maps the public `type` selector to its reference endpoint. */
4
+ const REFERENCE_PATHS = {
5
+ genres: '/genres',
6
+ genre_categories: '/genre-categories',
7
+ languages: '/languages',
8
+ contributor_roles: '/contributor-roles',
9
+ instruments: '/instruments',
10
+ distro_outlets: '/distro-outlets',
11
+ territories: '/territories',
12
+ };
13
+ const listReferenceData = {
14
+ name: 'list_reference_data',
15
+ toolset: 'reference',
16
+ gate: 'read',
17
+ title: 'List reference data',
18
+ description: 'Fetch a LabelGrid reference dataset used to resolve the IDs and codes that catalog and release tools expect. Pick ONE dataset with `type`: ' +
19
+ '`genres` and `genre_categories` (values for primary/secondary/tertiary genre IDs), `languages` (audio and metadata language codes), ' +
20
+ '`contributor_roles` (valid role names for track contributors), `instruments`, `distro_outlets` (the distribution outlets/stores available to your account), ' +
21
+ 'or `territories` (country/territory codes). Call this before creating or updating a release or track when you need a valid ID or code.',
22
+ inputShape: {
23
+ type: z.enum([
24
+ 'genres',
25
+ 'genre_categories',
26
+ 'languages',
27
+ 'contributor_roles',
28
+ 'instruments',
29
+ 'distro_outlets',
30
+ 'territories',
31
+ ]),
32
+ },
33
+ annotations: { readOnlyHint: true },
34
+ handler: (args, { client }) => client.get(REFERENCE_PATHS[args.type]),
35
+ };
36
+ export const referenceTools = [listReferenceData];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Release write toolset (safe writes): the release/track draft lifecycle plus
3
+ * validate, quality-report refresh, landing-page config, a short URL, and
4
+ * review-issue notes.
5
+ *
6
+ * create_release and create_track send an auto-generated Idempotency-Key so a
7
+ * retried creation cannot duplicate the entity. Release/track create+update
8
+ * forward a permissive `fields` object straight to the API, which owns all
9
+ * validation; the descriptions name the required fields.
10
+ */
11
+ import type { ToolDef } from './types.js';
12
+ export declare const releaseWriteTools: ToolDef[];
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Release write toolset (safe writes): the release/track draft lifecycle plus
3
+ * validate, quality-report refresh, landing-page config, a short URL, and
4
+ * review-issue notes.
5
+ *
6
+ * create_release and create_track send an auto-generated Idempotency-Key so a
7
+ * retried creation cannot duplicate the entity. Release/track create+update
8
+ * forward a permissive `fields` object straight to the API, which owns all
9
+ * validation; the descriptions name the required fields.
10
+ */
11
+ import { z } from 'zod';
12
+ /** A permissive body of API fields, forwarded verbatim to the endpoint. */
13
+ function fieldsBody(desc) {
14
+ return z.record(z.string(), z.unknown()).describe(desc);
15
+ }
16
+ /** Optional caller-supplied idempotency key, plumbed to the Idempotency-Key header. */
17
+ const idempotencyKey = z
18
+ .string()
19
+ .min(8)
20
+ .max(128)
21
+ .optional()
22
+ .describe('Optional idempotency key. The server deduplicates by this key for 24h — pass the SAME key when retrying a call whose outcome you did not observe. Without it, each call is a new operation.');
23
+ const createRelease = {
24
+ name: 'create_release',
25
+ toolset: 'releases',
26
+ gate: 'safe_write',
27
+ title: 'Create a release (draft)',
28
+ description: 'Create a new release in DRAFT state. Pass its metadata in `fields`. Required: content_type, label_id, artists, titles, cat (catalog number), artwork_ai_usage, primary_genre_id. Pass idempotency_key and reuse the SAME value if you retry a call whose outcome you did not observe — the server deduplicates by it for 24h; without a key each call is a new operation. Add tracks with create_track, then validate_release before distributing.',
29
+ inputShape: {
30
+ fields: fieldsBody('Release metadata. Required: content_type, label_id, artists, titles, cat, artwork_ai_usage, primary_genre_id. Many optional fields (dates, copyright lines, genres, per-outlet URLs) are supported — see the API docs.'),
31
+ idempotency_key: idempotencyKey,
32
+ },
33
+ annotations: {},
34
+ handler: (args, { client }) => client.post('/releases', args.fields, {
35
+ idempotency: true,
36
+ idempotencyKey: args.idempotency_key,
37
+ }),
38
+ };
39
+ const updateRelease = {
40
+ name: 'update_release',
41
+ toolset: 'releases',
42
+ gate: 'safe_write',
43
+ title: 'Update a release',
44
+ description: 'Update a release’s metadata. Supply only the fields you want to change in `fields`. Once a release has been submitted or distributed, some fields are locked: attempting to change a locked field returns a 403 with code RELEASE_LOCKED_FIELDS, surfaced verbatim so you can see exactly which fields cannot be changed.',
45
+ inputShape: {
46
+ release_id: z.number().int().positive(),
47
+ fields: fieldsBody('Release fields to change (same field set as create_release).'),
48
+ },
49
+ annotations: {},
50
+ handler: (args, { client }) => client.patch(`/releases/${args.release_id}`, args.fields),
51
+ };
52
+ const deleteRelease = {
53
+ name: 'delete_release',
54
+ toolset: 'releases',
55
+ gate: 'safe_write',
56
+ title: 'Delete a release',
57
+ description: 'Delete a release. The API only allows deleting a draft that has never been submitted; it refuses to delete a release that has been submitted or distributed.',
58
+ inputShape: { release_id: z.number().int().positive() },
59
+ annotations: { destructiveHint: true },
60
+ handler: (args, { client }) => client.delete(`/releases/${args.release_id}`),
61
+ };
62
+ const createTrack = {
63
+ name: 'create_track',
64
+ toolset: 'releases',
65
+ gate: 'safe_write',
66
+ title: 'Create a track',
67
+ description: 'Create a track on a release. Pass its metadata in `fields`. Required: release_id, disc, track_num, composition_type, artists, audio_ai_usage, composition_ai_usage, commercial_samples, audio_language, contributors, and recording_country (a required ISO 3166-1 alpha-2 country code, e.g. "US"). Pass idempotency_key and reuse the SAME value if you retry a call whose outcome you did not observe — the server deduplicates by it for 24h; without a key each call is a new operation.',
68
+ inputShape: {
69
+ fields: fieldsBody('Track metadata. Required: release_id, disc, track_num, composition_type, artists, audio_ai_usage, composition_ai_usage, commercial_samples, audio_language, contributors, recording_country (ISO 3166-1 alpha-2). Optional: titles, isrc, iswc, writers, publishers, splits, and more — see the API docs.'),
70
+ idempotency_key: idempotencyKey,
71
+ },
72
+ annotations: {},
73
+ handler: (args, { client }) => client.post('/tracks', args.fields, {
74
+ idempotency: true,
75
+ idempotencyKey: args.idempotency_key,
76
+ }),
77
+ };
78
+ const updateTrack = {
79
+ name: 'update_track',
80
+ toolset: 'releases',
81
+ gate: 'safe_write',
82
+ title: 'Update a track',
83
+ description: 'Update a track’s metadata. Supply only the fields you want to change in `fields`. As with releases, some fields lock once the parent release is submitted or distributed.',
84
+ inputShape: {
85
+ track_id: z.number().int().positive(),
86
+ fields: fieldsBody('Track fields to change (same field set as create_track).'),
87
+ },
88
+ annotations: {},
89
+ handler: (args, { client }) => client.patch(`/tracks/${args.track_id}`, args.fields),
90
+ };
91
+ const deleteTrack = {
92
+ name: 'delete_track',
93
+ toolset: 'releases',
94
+ gate: 'safe_write',
95
+ title: 'Delete a track',
96
+ description: 'Delete a track. Allowed while the parent release is an editable draft; the API refuses once the release is submitted or distributed.',
97
+ inputShape: { track_id: z.number().int().positive() },
98
+ annotations: { destructiveHint: true },
99
+ handler: (args, { client }) => client.delete(`/tracks/${args.track_id}`),
100
+ };
101
+ const validateRelease = {
102
+ name: 'validate_release',
103
+ toolset: 'releases',
104
+ gate: 'safe_write',
105
+ title: 'Validate a release',
106
+ description: 'Run validation on a release and return any problems that would block distribution, as both a human-readable `errors` list and a machine-readable `errors_structured` list. This is a near-read check: it changes nothing and is safe to repeat. Run it before distributing.',
107
+ inputShape: { release_id: z.number().int().positive() },
108
+ annotations: { idempotentHint: true },
109
+ handler: (args, { client }) => client.post(`/releases/${args.release_id}/validate`),
110
+ };
111
+ const refreshQualityReport = {
112
+ name: 'refresh_quality_report',
113
+ toolset: 'releases',
114
+ gate: 'safe_write',
115
+ title: 'Refresh the quality report',
116
+ description: 'Re-run the Preflight QC automated checks and refresh the release’s quality report. Read the results with get_quality_report. The server applies an hourly refresh budget, so frequent calls may be rate-limited. Preflight QC is an optional add-on.',
117
+ inputShape: { release_id: z.number().int().positive() },
118
+ annotations: { idempotentHint: true },
119
+ handler: (args, { client }) => client.post(`/releases/${args.release_id}/quality-report/refresh`),
120
+ };
121
+ const updateLandingConfig = {
122
+ name: 'update_landing_config',
123
+ toolset: 'releases',
124
+ gate: 'safe_write',
125
+ title: 'Update a release landing-page config',
126
+ description: 'Set the smart-link landing-page configuration for a release. `actions` uses the current (v2) action-list contract — each entry describes one call-to-action on the page. You can also set links_page_enabled, config_mode, page_style, custom_cta_text, custom_description, and pre_order_links. This replaces the landing configuration.',
127
+ inputShape: {
128
+ release_id: z.number().int().positive(),
129
+ links_page_enabled: z
130
+ .union([z.boolean(), z.string()])
131
+ .optional()
132
+ .describe('Whether the smart-link page is enabled.'),
133
+ config_mode: z.string().optional(),
134
+ page_style: z.string().optional(),
135
+ custom_cta_text: z.string().optional(),
136
+ custom_description: z.string().optional(),
137
+ actions: z
138
+ .array(z.record(z.string(), z.unknown()))
139
+ .optional()
140
+ .describe('The v2 action list — one object per call-to-action on the landing page.'),
141
+ pre_order_links: z.array(z.record(z.string(), z.unknown())).optional(),
142
+ },
143
+ annotations: {},
144
+ handler: (args, { client }) => {
145
+ const { release_id, ...body } = args;
146
+ return client.put(`/releases/${release_id}/landing-config`, body);
147
+ },
148
+ };
149
+ const createReleaseShortUrl = {
150
+ name: 'create_release_short_url',
151
+ toolset: 'releases',
152
+ gate: 'safe_write',
153
+ title: 'Create a release short URL',
154
+ description: 'Create (or return the existing) short URL for a release’s smart-link landing page. Safe to repeat.',
155
+ inputShape: { release_id: z.number().int().positive() },
156
+ annotations: { idempotentHint: true },
157
+ handler: (args, { client }) => client.post('/releases/short-url', { release_id: args.release_id }),
158
+ };
159
+ const addReviewIssueNote = {
160
+ name: 'add_review_issue_note',
161
+ toolset: 'releases',
162
+ gate: 'safe_write',
163
+ title: 'Add a note to a review issue',
164
+ description: 'Add a note to a release review issue — for example to explain a fix or add context for the reviewer. `review_issue_id` is the id of the issue (from list_review_issues).',
165
+ inputShape: {
166
+ review_issue_id: z.number().int().positive(),
167
+ note: z.string().describe('The note text to attach to the issue.'),
168
+ },
169
+ annotations: {},
170
+ handler: (args, { client }) => client.post(`/review-issues/${args.review_issue_id}/notes`, { note: args.note }),
171
+ };
172
+ export const releaseWriteTools = [
173
+ createRelease,
174
+ updateRelease,
175
+ deleteRelease,
176
+ createTrack,
177
+ updateTrack,
178
+ deleteTrack,
179
+ validateRelease,
180
+ refreshQualityReport,
181
+ updateLandingConfig,
182
+ createReleaseShortUrl,
183
+ addReviewIssueNote,
184
+ ];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Review reads: release quality-check issues, the issue-definition catalog,
3
+ * the Preflight QC quality report, and Stream Radar early-warning flags. All
4
+ * read-only, in the `review` toolset.
5
+ */
6
+ import type { ToolDef } from './types.js';
7
+ export declare const reviewReadTools: ToolDef[];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Review reads: release quality-check issues, the issue-definition catalog,
3
+ * the Preflight QC quality report, and Stream Radar early-warning flags. All
4
+ * read-only, in the `review` toolset.
5
+ */
6
+ import { z } from 'zod';
7
+ const listReviewIssues = {
8
+ name: 'list_review_issues',
9
+ toolset: 'review',
10
+ gate: 'read',
11
+ title: 'List release review issues',
12
+ description: 'List the review issues raised against a release during its automated quality checks. `release_id` is required. Each issue carries a code (see list_issue_definitions for what each code means), severity, and whether it blocks distribution. Use this to see what a customer must fix before a release can go out.',
13
+ inputShape: {
14
+ release_id: z.number().int().positive().describe('The release whose issues to list. Required.'),
15
+ },
16
+ annotations: { readOnlyHint: true },
17
+ handler: (args, { client }) => client.get('/review-issues', { release_id: args.release_id }),
18
+ };
19
+ const listIssueDefinitions = {
20
+ name: 'list_issue_definitions',
21
+ toolset: 'review',
22
+ gate: 'read',
23
+ title: 'List issue definitions',
24
+ description: 'Retrieve the catalog of review issue definitions: each code’s human-readable title, description, severity and whether it blocks distribution. Use it to interpret the codes returned by list_review_issues and the quality report. Issue codes are string slugs.',
25
+ inputShape: {},
26
+ annotations: { readOnlyHint: true },
27
+ handler: (_args, { client }) => client.get('/issue-definitions'),
28
+ };
29
+ const getQualityReport = {
30
+ name: 'get_quality_report',
31
+ toolset: 'review',
32
+ gate: 'read',
33
+ title: 'Get a release quality report',
34
+ description: 'Retrieve the Preflight QC quality report for a release: the customer-facing issues found by the automated checks so you can review them before confirming the release into distribution. Preflight QC is an optional add-on — if your account does not have it enabled the API returns a 403, which is surfaced verbatim.',
35
+ inputShape: { release_id: z.number().int().positive() },
36
+ annotations: { readOnlyHint: true },
37
+ handler: (args, { client }) => client.get(`/releases/${args.release_id}/quality-report`),
38
+ };
39
+ const listStreamRadarFlags = {
40
+ name: 'list_stream_radar_flags',
41
+ toolset: 'review',
42
+ gate: 'read',
43
+ title: 'List Stream Radar flags',
44
+ description: 'List Stream Radar flags for your releases, paginated — early-warning flags from streaming-integrity monitoring that surface possible artificial-streaming activity so you can act early. Filter by status, severity, dsp, isrc, release_id, and the last-detected date range (detected_from/detected_to). Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim.',
45
+ inputShape: {
46
+ page: z.number().int().positive().optional(),
47
+ per_page: z.number().int().positive().optional(),
48
+ status: z.string().optional().describe('Filter by flag status.'),
49
+ severity: z.string().optional().describe('Filter by severity.'),
50
+ dsp: z.string().optional().describe('Filter by platform/DSP.'),
51
+ isrc: z.string().optional(),
52
+ release_id: z.number().int().positive().optional(),
53
+ detected_from: z.string().optional().describe('Earliest last-detected date, YYYY-MM-DD.'),
54
+ detected_to: z.string().optional().describe('Latest last-detected date, YYYY-MM-DD.'),
55
+ },
56
+ annotations: { readOnlyHint: true },
57
+ handler: (args, { client }) => {
58
+ const { page, per_page, ...filter } = args;
59
+ return client.get('/stream-radar/flags', { page, per_page, filter });
60
+ },
61
+ };
62
+ const getStreamRadarFlag = {
63
+ name: 'get_stream_radar_flag',
64
+ toolset: 'review',
65
+ gate: 'read',
66
+ title: 'Get a Stream Radar flag',
67
+ description: 'Retrieve one Stream Radar flag by id, with its full detail. Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim.',
68
+ inputShape: { flag_id: z.number().int().positive() },
69
+ annotations: { readOnlyHint: true },
70
+ handler: (args, { client }) => client.get(`/stream-radar/flags/${args.flag_id}`),
71
+ };
72
+ export const reviewReadTools = [
73
+ listReviewIssues,
74
+ listIssueDefinitions,
75
+ getQualityReport,
76
+ listStreamRadarFlags,
77
+ getStreamRadarFlag,
78
+ ];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Setup toolset: the single tool exposed when no API token is configured.
3
+ *
4
+ * In setup mode the server registers ONLY this tool. It makes no API calls —
5
+ * it returns a structured, human-relayable guide that the AI client walks the
6
+ * user through to create a LabelGrid API token and add it to their client
7
+ * configuration. The `setup` toolset is deliberately NOT part of the tool
8
+ * catalog and is not user-selectable via LABELGRID_TOOLSETS.
9
+ */
10
+ import type { ToolDef } from './types.js';
11
+ export declare const setupTools: ToolDef[];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Setup toolset: the single tool exposed when no API token is configured.
3
+ *
4
+ * In setup mode the server registers ONLY this tool. It makes no API calls —
5
+ * it returns a structured, human-relayable guide that the AI client walks the
6
+ * user through to create a LabelGrid API token and add it to their client
7
+ * configuration. The `setup` toolset is deliberately NOT part of the tool
8
+ * catalog and is not user-selectable via LABELGRID_TOOLSETS.
9
+ */
10
+ // Mirrors the README Claude Desktop / Cursor snippet exactly (placeholder token).
11
+ const CLIENT_CONFIG_JSON = `{
12
+ "mcpServers": {
13
+ "labelgrid": {
14
+ "command": "npx",
15
+ "args": ["-y", "@labelgrid/mcp"],
16
+ "env": {
17
+ "LABELGRID_API_TOKEN": "your-token-here"
18
+ }
19
+ }
20
+ }
21
+ }`;
22
+ /** The structured guide returned to the client, relayed to the user in chat. */
23
+ const SETUP_GUIDE = {
24
+ status: 'not_connected',
25
+ summary: 'This LabelGrid MCP server is running in setup mode because no API token is configured yet. Walk the user through the steps below so their AI client can connect to their LabelGrid account. Relay the steps in plain language and never ask the user to paste their token into the chat.',
26
+ steps: [
27
+ 'Make sure the account has API access: it is part of LabelGrid API plans — see the API Overview and Quickstart (https://help.labelgrid.com/en/integrations/api-overview). If the API Tokens page is missing from the dashboard, the account does not have API access yet.',
28
+ 'Log in to your LabelGrid dashboard and open Profile → API Tokens (https://app.labelgrid.com/user/profile/api-tokens).',
29
+ 'Create a new token and copy it.',
30
+ 'Add the token to your MCP client configuration as the LABELGRID_API_TOKEN environment variable (do NOT paste the token into this chat).',
31
+ 'Restart your MCP client (or the server) — the full toolset loads automatically once the token is configured.',
32
+ ],
33
+ config_examples: {
34
+ claude_desktop: CLIENT_CONFIG_JSON,
35
+ claude_code: 'claude mcp add labelgrid -e LABELGRID_API_TOKEN=your-token-here -- npx -y @labelgrid/mcp',
36
+ cursor: CLIENT_CONFIG_JSON,
37
+ },
38
+ security_note: 'Never paste your API token into the chat — it belongs only in your client configuration file. Anyone with the token can access your account until you revoke it in the dashboard.',
39
+ optional_settings: [
40
+ 'LABELGRID_ENABLE_WRITES — safe draft-stage writes; on by default, set false for read-only (see the README Safety section).',
41
+ 'LABELGRID_ENABLE_FULL_WRITES (plus LABELGRID_FULL_WRITES_ACK) — arm consequential distribution actions; off by default (see the README Safety section).',
42
+ 'LABELGRID_TOOLSETS — expose only a comma-separated subset of toolsets (see the README Safety section).',
43
+ 'LABELGRID_READ_ONLY — force reads only, overriding the write flags (see the README Safety section).',
44
+ ],
45
+ };
46
+ const setup = {
47
+ name: 'setup',
48
+ toolset: 'setup',
49
+ gate: 'read',
50
+ title: 'Set up the LabelGrid connection',
51
+ description: 'This LabelGrid MCP server is not connected to an account yet because no API token is configured. Call this tool to get step-by-step instructions to walk the user through creating a LabelGrid API token and adding it to their MCP client configuration. Returns the setup steps, ready-to-copy client config examples (with a placeholder token), a security note, and the optional settings. Makes no API calls.',
52
+ inputShape: {},
53
+ annotations: { readOnlyHint: true },
54
+ handler: () => Promise.resolve({ data: SETUP_GUIDE }),
55
+ };
56
+ export const setupTools = [setup];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The declarative tool contract every tool family produces, plus the
3
+ * {@link ApiResult}-to-MCP result mapper.
4
+ *
5
+ * A tool is a plain data declaration — name, gate, zod input shape, client-hint
6
+ * annotations and a one-call handler. The server module turns each declaration
7
+ * into a registered MCP tool. This keeps every tool a thin wrapper: one HTTP
8
+ * call, no client-side business logic.
9
+ */
10
+ import type { z } from 'zod';
11
+ import type { ApiResult, LabelGridClient } from '../api/http.js';
12
+ import type { Config } from '../config.js';
13
+ import type { Gate } from '../gating.js';
14
+ export type ToolAnnotations = {
15
+ readOnlyHint?: boolean;
16
+ destructiveHint?: boolean;
17
+ idempotentHint?: boolean;
18
+ };
19
+ export type ToolContext = {
20
+ client: LabelGridClient;
21
+ config: Config;
22
+ };
23
+ export type ToolDef = {
24
+ name: string;
25
+ toolset: string;
26
+ gate: Gate;
27
+ title: string;
28
+ description: string;
29
+ inputShape: z.ZodRawShape;
30
+ annotations: ToolAnnotations;
31
+ handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<ApiResult<unknown>>;
32
+ };
33
+ export type ToolResult = {
34
+ content: [{
35
+ type: 'text';
36
+ text: string;
37
+ }];
38
+ isError?: true;
39
+ };
40
+ /** Maps an {@link ApiResult} to an MCP tool result: pretty JSON, error flagged. */
41
+ export declare function toToolResult(r: ApiResult<unknown>): ToolResult;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The declarative tool contract every tool family produces, plus the
3
+ * {@link ApiResult}-to-MCP result mapper.
4
+ *
5
+ * A tool is a plain data declaration — name, gate, zod input shape, client-hint
6
+ * annotations and a one-call handler. The server module turns each declaration
7
+ * into a registered MCP tool. This keeps every tool a thin wrapper: one HTTP
8
+ * call, no client-side business logic.
9
+ */
10
+ /** Hard ceiling on the serialized text of a single tool result, in characters. */
11
+ const MAX_TOOL_TEXT = 400_000;
12
+ /** Maps an {@link ApiResult} to an MCP tool result: pretty JSON, error flagged. */
13
+ export function toToolResult(r) {
14
+ if ('data' in r) {
15
+ const text = JSON.stringify(r.data ?? null, null, 2);
16
+ if (text.length > MAX_TOOL_TEXT) {
17
+ // Reserve headroom for the wrapper keys so the whole envelope, not just the
18
+ // prefix, stays under the ceiling.
19
+ const wrapped = JSON.stringify({
20
+ truncated: true,
21
+ note: 'Response truncated — use pagination or filters to narrow the request.',
22
+ data_prefix: text.slice(0, MAX_TOOL_TEXT - 1_000),
23
+ }, null, 2);
24
+ return { content: [{ type: 'text', text: wrapped }] };
25
+ }
26
+ return { content: [{ type: 'text', text }] };
27
+ }
28
+ const errorText = JSON.stringify({ error: r.error }, null, 2);
29
+ if (errorText.length <= MAX_TOOL_TEXT) {
30
+ return { content: [{ type: 'text', text: errorText }], isError: true };
31
+ }
32
+ // The only unbounded fields are the verbatim API passthroughs; drop them so the
33
+ // envelope stays under the ceiling while the diagnostic core (code/message/
34
+ // status/suggestion) survives intact.
35
+ const bounded = { ...r.error };
36
+ if (bounded.errors !== undefined)
37
+ bounded.errors = '[truncated]';
38
+ if (bounded.errors_structured !== undefined)
39
+ bounded.errors_structured = '[truncated]';
40
+ const boundedText = JSON.stringify({ error: bounded }, null, 2);
41
+ if (boundedText.length <= MAX_TOOL_TEXT) {
42
+ return { content: [{ type: 'text', text: boundedText }], isError: true };
43
+ }
44
+ // Even after dropping the passthroughs the envelope is over the ceiling — a
45
+ // hostile error whose own code/message is huge. Hard-slice the serialized text
46
+ // at MAX_TOOL_TEXT and return it as-is: the result is no longer valid JSON, but
47
+ // an over-limit hostile error forfeits pretty structure to keep the hard bound.
48
+ return {
49
+ content: [{ type: 'text', text: boundedText.slice(0, MAX_TOOL_TEXT) }],
50
+ isError: true,
51
+ };
52
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Webhooks toolset: read your webhook subscriptions and delivery logs, and
3
+ * manage them (create/update/delete/test/rotate-secret). Reads are always on;
4
+ * the mutations are safe writes.
5
+ */
6
+ import type { ToolDef } from './types.js';
7
+ export declare const webhookTools: ToolDef[];
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Webhooks toolset: read your webhook subscriptions and delivery logs, and
3
+ * manage them (create/update/delete/test/rotate-secret). Reads are always on;
4
+ * the mutations are safe writes.
5
+ */
6
+ import { z } from 'zod';
7
+ const webhookId = z.number().int().positive().describe('The webhook id.');
8
+ const eventsShape = z
9
+ .record(z.string(), z.unknown())
10
+ .describe('The event subscription object selecting which event types this webhook receives. Call list_webhook_event_types for the available types and each payload shape.');
11
+ const listWebhooks = {
12
+ name: 'list_webhooks',
13
+ toolset: 'webhooks',
14
+ gate: 'read',
15
+ title: 'List webhooks',
16
+ description: 'List the webhook subscriptions configured on your account, each with its URL, subscribed events and active state.',
17
+ inputShape: {},
18
+ annotations: { readOnlyHint: true },
19
+ handler: (_args, { client }) => client.get('/webhooks'),
20
+ };
21
+ const getWebhook = {
22
+ name: 'get_webhook',
23
+ toolset: 'webhooks',
24
+ gate: 'read',
25
+ title: 'Get a webhook',
26
+ description: 'Retrieve one webhook subscription by id.',
27
+ inputShape: { webhook_id: webhookId },
28
+ annotations: { readOnlyHint: true },
29
+ handler: (args, { client }) => client.get(`/webhooks/${args.webhook_id}`),
30
+ };
31
+ const getWebhookLogs = {
32
+ name: 'get_webhook_logs',
33
+ toolset: 'webhooks',
34
+ gate: 'read',
35
+ title: 'Get webhook delivery logs',
36
+ description: 'Retrieve the recent delivery log for a webhook — the attempts, response codes and outcomes — to debug why events did or did not reach your endpoint.',
37
+ inputShape: { webhook_id: webhookId },
38
+ annotations: { readOnlyHint: true },
39
+ handler: (args, { client }) => client.get(`/webhooks/${args.webhook_id}/logs`),
40
+ };
41
+ const listWebhookEventTypes = {
42
+ name: 'list_webhook_event_types',
43
+ toolset: 'webhooks',
44
+ gate: 'read',
45
+ title: 'List webhook event types',
46
+ description: 'List every available webhook event type, each with the schema of the payload it delivers. Use it to decide which events to subscribe a webhook to.',
47
+ inputShape: {},
48
+ annotations: { readOnlyHint: true },
49
+ handler: (_args, { client }) => client.get('/webhooks/event-types'),
50
+ };
51
+ const createWebhook = {
52
+ name: 'create_webhook',
53
+ toolset: 'webhooks',
54
+ gate: 'safe_write',
55
+ title: 'Create a webhook',
56
+ description: 'Create a webhook subscription. `name` and `url` (the HTTPS endpoint that will receive events) are required, along with `events` selecting which event types to deliver. The API returns a signing secret once on creation — store it to verify incoming payloads.',
57
+ inputShape: {
58
+ name: z.string().describe('A label for this webhook.'),
59
+ url: z.string().describe('The HTTPS endpoint that will receive event deliveries.'),
60
+ events: eventsShape,
61
+ },
62
+ annotations: {},
63
+ handler: (args, { client }) => client.post('/webhooks', { name: args.name, url: args.url, events: args.events }),
64
+ };
65
+ const updateWebhook = {
66
+ name: 'update_webhook',
67
+ toolset: 'webhooks',
68
+ gate: 'safe_write',
69
+ title: 'Update a webhook',
70
+ description: 'Update a webhook subscription. Supply only the fields you want to change: `name`, `url`, `events`, or `is_active` (set false to pause deliveries).',
71
+ inputShape: {
72
+ webhook_id: webhookId,
73
+ name: z.string().optional(),
74
+ url: z.string().optional(),
75
+ events: eventsShape.optional(),
76
+ is_active: z.boolean().optional().describe('Set false to pause deliveries.'),
77
+ },
78
+ annotations: {},
79
+ handler: (args, { client }) => {
80
+ const { webhook_id, ...body } = args;
81
+ return client.patch(`/webhooks/${webhook_id}`, body);
82
+ },
83
+ };
84
+ const deleteWebhook = {
85
+ name: 'delete_webhook',
86
+ toolset: 'webhooks',
87
+ gate: 'safe_write',
88
+ title: 'Delete a webhook',
89
+ description: 'Delete a webhook subscription permanently. It will stop receiving events.',
90
+ inputShape: { webhook_id: webhookId },
91
+ annotations: { destructiveHint: true },
92
+ handler: (args, { client }) => client.delete(`/webhooks/${args.webhook_id}`),
93
+ };
94
+ const testWebhook = {
95
+ name: 'test_webhook',
96
+ toolset: 'webhooks',
97
+ gate: 'safe_write',
98
+ title: 'Send a test webhook event',
99
+ description: 'Send a test event to a webhook’s endpoint so you can confirm it is reachable and your signature verification works. Safe to repeat.',
100
+ inputShape: { webhook_id: webhookId },
101
+ annotations: { idempotentHint: true },
102
+ handler: (args, { client }) => client.post(`/webhooks/${args.webhook_id}/test`),
103
+ };
104
+ const rotateWebhookSecret = {
105
+ name: 'rotate_webhook_secret',
106
+ toolset: 'webhooks',
107
+ gate: 'safe_write',
108
+ title: 'Rotate a webhook signing secret',
109
+ description: 'Generate a new signing secret for a webhook and return it. WARNING: the old secret stops working immediately — update your endpoint’s signature verification with the new secret right away or deliveries will fail verification.',
110
+ inputShape: { webhook_id: webhookId },
111
+ annotations: { destructiveHint: true },
112
+ handler: (args, { client }) => client.post(`/webhooks/${args.webhook_id}/regenerate-secret`),
113
+ };
114
+ export const webhookTools = [
115
+ listWebhooks,
116
+ getWebhook,
117
+ getWebhookLogs,
118
+ listWebhookEventTypes,
119
+ createWebhook,
120
+ updateWebhook,
121
+ deleteWebhook,
122
+ testWebhook,
123
+ rotateWebhookSecret,
124
+ ];
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The package version, read from package.json at runtime so it stays the single
3
+ * source of truth. Resolved relative to this module: `dist/version.js` → the
4
+ * package-root `package.json` when built, `src/version.ts` → repo-root in tests.
5
+ */
6
+ export declare const VERSION: string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The package version, read from package.json at runtime so it stays the single
3
+ * source of truth. Resolved relative to this module: `dist/version.js` → the
4
+ * package-root `package.json` when built, `src/version.ts` → repo-root in tests.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ function readVersion() {
8
+ try {
9
+ const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
10
+ const pkg = JSON.parse(raw);
11
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
12
+ }
13
+ catch {
14
+ return '0.0.0';
15
+ }
16
+ }
17
+ export const VERSION = readVersion();