@labelgrid/mcp 0.2.2 → 0.3.1

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 (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +132 -100
  3. package/dist/config.d.ts +15 -0
  4. package/dist/config.js +38 -6
  5. package/dist/coverage.js +79 -79
  6. package/dist/gating.d.ts +1 -1
  7. package/dist/gating.js +5 -1
  8. package/dist/index.js +1 -2
  9. package/dist/projection.d.ts +40 -0
  10. package/dist/projection.js +145 -0
  11. package/dist/resources.d.ts +29 -0
  12. package/dist/resources.js +107 -0
  13. package/dist/server.d.ts +2 -2
  14. package/dist/server.js +24 -4
  15. package/dist/tools/account.d.ts +3 -0
  16. package/dist/tools/account.js +32 -0
  17. package/dist/tools/all.js +12 -20
  18. package/dist/tools/catalog.d.ts +14 -0
  19. package/dist/tools/catalog.js +266 -0
  20. package/dist/tools/distribution.d.ts +13 -0
  21. package/dist/tools/distribution.js +265 -0
  22. package/dist/tools/finance.d.ts +12 -0
  23. package/dist/tools/finance.js +311 -0
  24. package/dist/tools/insights.d.ts +7 -0
  25. package/dist/tools/insights.js +139 -0
  26. package/dist/tools/reference.js +9 -24
  27. package/dist/tools/releases.d.ts +11 -0
  28. package/dist/tools/releases.js +177 -0
  29. package/dist/tools/setup.js +1 -1
  30. package/dist/tools/types.d.ts +1 -1
  31. package/dist/tools/webhooks.d.ts +3 -3
  32. package/dist/tools/webhooks.js +73 -105
  33. package/package.json +5 -14
  34. package/server.json +3 -3
  35. package/dist/api/content-types.d.ts +0 -33
  36. package/dist/api/content-types.js +0 -87
  37. package/dist/api/http.d.ts +0 -74
  38. package/dist/api/http.js +0 -392
  39. package/dist/api/upload.d.ts +0 -26
  40. package/dist/api/upload.js +0 -104
  41. package/dist/log.d.ts +0 -16
  42. package/dist/log.js +0 -35
  43. package/dist/tools/accounting.d.ts +0 -12
  44. package/dist/tools/accounting.js +0 -386
  45. package/dist/tools/analytics.d.ts +0 -3
  46. package/dist/tools/analytics.js +0 -62
  47. package/dist/tools/catalog-read.d.ts +0 -10
  48. package/dist/tools/catalog-read.js +0 -145
  49. package/dist/tools/catalog-write.d.ts +0 -12
  50. package/dist/tools/catalog-write.js +0 -206
  51. package/dist/tools/delivery.d.ts +0 -6
  52. package/dist/tools/delivery.js +0 -40
  53. package/dist/tools/files-read.d.ts +0 -7
  54. package/dist/tools/files-read.js +0 -86
  55. package/dist/tools/full-writes.d.ts +0 -12
  56. package/dist/tools/full-writes.js +0 -248
  57. package/dist/tools/identity.d.ts +0 -3
  58. package/dist/tools/identity.js +0 -28
  59. package/dist/tools/release-write.d.ts +0 -12
  60. package/dist/tools/release-write.js +0 -184
  61. package/dist/tools/review-read.d.ts +0 -7
  62. package/dist/tools/review-read.js +0 -78
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Releases toolset: release-level reads (review results, the delivery queue,
3
+ * the smart-link landing config, track licenses) plus the safe-write release
4
+ * checks, landing-page management and review-issue notes.
5
+ *
6
+ * The two [proj] reads (`get_release_review`, `get_delivery_queue`) default to
7
+ * concise-mode projection; `response_format: 'detailed'` returns the verbatim
8
+ * API response.
9
+ */
10
+ import { z } from 'zod';
11
+ import { applyProjection } from '../projection.js';
12
+ const releaseId = z.number().int().positive().describe('The release id.');
13
+ const responseFormat = z
14
+ .enum(['concise', 'detailed'])
15
+ .optional()
16
+ .describe("'concise' (default) keeps only the high-signal fields (ids always kept); 'detailed' returns the verbatim API response.");
17
+ const getReleaseReview = {
18
+ name: 'get_release_review',
19
+ toolset: 'releases',
20
+ gate: 'read',
21
+ title: 'Get release review results',
22
+ description: "Read a release's automated quality-check results. Pick ONE view with `view`: " +
23
+ '`issues` lists the review issues raised against the release — each carries a code (see list_reference_data type issue_definitions), severity, and whether it blocks distribution; use it to see what must be fixed before the release can go out. ' +
24
+ '`quality_report` retrieves the Preflight QC quality report — the customer-facing issues found by the automated checks, to review before confirming the release into distribution; Preflight QC is an optional add-on — without it the API returns a 403, surfaced verbatim. ' +
25
+ "response_format:'detailed' returns the verbatim API response.",
26
+ inputShape: {
27
+ release_id: releaseId,
28
+ view: z.enum(['issues', 'quality_report']).describe('Which review read.'),
29
+ response_format: responseFormat,
30
+ },
31
+ annotations: { readOnlyHint: true },
32
+ handler: async (args, { client }) => {
33
+ const result = args.view === 'quality_report'
34
+ ? await client.get(`/releases/${args.release_id}/quality-report`)
35
+ : await client.get('/review-issues', { release_id: args.release_id });
36
+ return applyProjection(result, 'get_release_review', args.response_format);
37
+ },
38
+ };
39
+ const getDeliveryQueue = {
40
+ name: 'get_delivery_queue',
41
+ toolset: 'releases',
42
+ gate: 'read',
43
+ title: 'Get the distribution queue',
44
+ description: 'List the distribution queue entries for your account, paginated — one entry per (release, outlet) delivery with its current status (e.g. pending review, processing, scheduled, complete, error) — where a release is in the delivery pipeline to each store. Filter by `release_id`, `outlet_id`, or `status`. ' +
45
+ "response_format:'detailed' returns the verbatim API response.",
46
+ inputShape: {
47
+ release_id: z.number().int().positive().optional().describe('Filter to one release.'),
48
+ outlet_id: z.number().int().positive().optional().describe('Filter to one outlet/store.'),
49
+ status: z.string().optional().describe('Filter by delivery status.'),
50
+ page: z.number().int().positive().optional(),
51
+ per_page: z.number().int().positive().optional(),
52
+ response_format: responseFormat,
53
+ },
54
+ annotations: { readOnlyHint: true },
55
+ handler: async (args, { client }) => {
56
+ const result = await client.get('/queues/distro', {
57
+ page: args.page,
58
+ per_page: args.per_page,
59
+ filter: {
60
+ release_id: args.release_id,
61
+ outlet_id: args.outlet_id,
62
+ status: args.status,
63
+ },
64
+ });
65
+ return applyProjection(result, 'get_delivery_queue', args.response_format);
66
+ },
67
+ };
68
+ const getLandingConfig = {
69
+ name: 'get_landing_config',
70
+ toolset: 'releases',
71
+ gate: 'read',
72
+ title: 'Get a release landing-page config',
73
+ description: 'Retrieve the smart-link landing-page configuration for a release: whether the links page is enabled, its style/mode, custom copy, the action list and any pre-order links. Pair with manage_release_links (action update_landing_config) to change it.',
74
+ inputShape: { release_id: releaseId },
75
+ annotations: { readOnlyHint: true },
76
+ handler: (args, { client }) => client.get(`/releases/${args.release_id}/landing-config`),
77
+ };
78
+ const listTrackLicenses = {
79
+ name: 'list_track_licenses',
80
+ toolset: 'releases',
81
+ gate: 'read',
82
+ title: 'List track licenses',
83
+ description: 'List the licenses attached to a track (e.g. cover/mechanical or sample clearances), paginated. Pass `license_id` to retrieve one license by its id instead.',
84
+ inputShape: {
85
+ track_id: z.number().int().positive().describe('The track id.'),
86
+ license_id: z
87
+ .number()
88
+ .int()
89
+ .positive()
90
+ .optional()
91
+ .describe('Retrieve one license by id instead of listing.'),
92
+ page: z.number().int().positive().optional(),
93
+ per_page: z.number().int().positive().optional(),
94
+ },
95
+ annotations: { readOnlyHint: true },
96
+ handler: (args, { client }) => {
97
+ if (args.license_id !== undefined) {
98
+ return client.get(`/tracks/${args.track_id}/licenses/${args.license_id}`);
99
+ }
100
+ return client.get(`/tracks/${args.track_id}/licenses`, {
101
+ page: args.page,
102
+ per_page: args.per_page,
103
+ });
104
+ },
105
+ };
106
+ const runReleaseChecks = {
107
+ name: 'run_release_checks',
108
+ toolset: 'releases',
109
+ gate: 'safe_write',
110
+ title: 'Run release checks',
111
+ description: 'Run an automated check on a release. Pick ONE with `check`: ' +
112
+ '`validate` returns any problems that would block distribution, as a human-readable `errors` list and a machine-readable `errors_structured` list — it changes nothing and is safe to repeat; run it before distributing. ' +
113
+ '`refresh_quality_report` re-runs the Preflight QC checks and refreshes the quality report (read it with get_release_review view quality_report); the server applies an hourly refresh budget, so frequent calls may be rate-limited. Preflight QC is an optional add-on.',
114
+ inputShape: {
115
+ release_id: releaseId,
116
+ check: z.enum(['validate', 'refresh_quality_report']).describe('Which check to run.'),
117
+ },
118
+ annotations: { idempotentHint: true },
119
+ handler: (args, { client }) => args.check === 'refresh_quality_report'
120
+ ? client.post(`/releases/${args.release_id}/quality-report/refresh`)
121
+ : client.post(`/releases/${args.release_id}/validate`),
122
+ };
123
+ const manageReleaseLinks = {
124
+ name: 'manage_release_links',
125
+ toolset: 'releases',
126
+ gate: 'safe_write',
127
+ title: 'Manage a release smart link',
128
+ description: "Manage a release's smart-link landing page. Pick ONE action with `action`: " +
129
+ '`update_landing_config` replaces the landing-page configuration with `config` (required for this action) — `config.actions` uses the current (v2) action-list contract (one entry per call-to-action); other keys: links_page_enabled, config_mode, page_style, custom_cta_text, custom_description, pre_order_links. ' +
130
+ "`create_short_url` creates (or returns the existing) short URL for the release's smart-link landing page — safe to repeat.",
131
+ inputShape: {
132
+ release_id: releaseId,
133
+ action: z.enum(['update_landing_config', 'create_short_url']).describe('Which action.'),
134
+ config: z
135
+ .record(z.string(), z.unknown())
136
+ .optional()
137
+ .describe('The landing-page configuration to set (required for update_landing_config).'),
138
+ },
139
+ annotations: {},
140
+ handler: (args, { client }) => {
141
+ if (args.action === 'create_short_url') {
142
+ return client.post('/releases/short-url', { release_id: args.release_id });
143
+ }
144
+ if (args.config === undefined) {
145
+ return Promise.resolve({
146
+ error: {
147
+ code: 'INVALID_SELECTOR',
148
+ message: "action 'update_landing_config' requires `config` — the landing-page configuration to set.",
149
+ status: 0,
150
+ },
151
+ });
152
+ }
153
+ return client.put(`/releases/${args.release_id}/landing-config`, args.config);
154
+ },
155
+ };
156
+ const addReviewIssueNote = {
157
+ name: 'add_review_issue_note',
158
+ toolset: 'releases',
159
+ gate: 'safe_write',
160
+ title: 'Add a note to a review issue',
161
+ description: 'Add a note to a release review issue — to explain a fix or add reviewer context. `review_issue_id` comes from get_release_review view issues.',
162
+ inputShape: {
163
+ review_issue_id: z.number().int().positive(),
164
+ note: z.string().describe('The note text to attach to the issue.'),
165
+ },
166
+ annotations: {},
167
+ handler: (args, { client }) => client.post(`/review-issues/${args.review_issue_id}/notes`, { note: args.note }),
168
+ };
169
+ export const releaseTools = [
170
+ getReleaseReview,
171
+ getDeliveryQueue,
172
+ getLandingConfig,
173
+ listTrackLicenses,
174
+ runReleaseChecks,
175
+ manageReleaseLinks,
176
+ addReviewIssueNote,
177
+ ];
@@ -39,7 +39,7 @@ const SETUP_GUIDE = {
39
39
  optional_settings: [
40
40
  'LABELGRID_ENABLE_WRITES — safe draft-stage writes; on by default, set false for read-only (see the README Safety section).',
41
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).',
42
+ 'LABELGRID_TOOLSETS — expose only a comma-separated subset of toolsets: account, reference, catalog, releases, insights, finance, webhooks, distribution (pre-0.3.0 names are still accepted as aliases). The webhooks toolset is off by default — name it explicitly here to enable it (see the README Configuration section).',
43
43
  'LABELGRID_READ_ONLY — force reads only, overriding the write flags (see the README Safety section).',
44
44
  ],
45
45
  };
@@ -7,8 +7,8 @@
7
7
  * into a registered MCP tool. This keeps every tool a thin wrapper: one HTTP
8
8
  * call, no client-side business logic.
9
9
  */
10
+ import type { ApiResult, LabelGridClient } from '@labelgrid/core';
10
11
  import type { z } from 'zod';
11
- import type { ApiResult, LabelGridClient } from '../api/http.js';
12
12
  import type { Config } from '../config.js';
13
13
  import type { Gate } from '../gating.js';
14
14
  export type ToolAnnotations = {
@@ -1,7 +1,7 @@
1
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.
2
+ * Webhooks toolset: read your webhook subscriptions and delivery logs
3
+ * (list_webhooks) and manage them (manage_webhook: create/update/delete/test/
4
+ * rotate_secret). The read is always on; the mutations are safe writes.
5
5
  */
6
6
  import type { ToolDef } from './types.js';
7
7
  export declare const webhookTools: ToolDef[];
@@ -1,124 +1,92 @@
1
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.
2
+ * Webhooks toolset: read your webhook subscriptions and delivery logs
3
+ * (list_webhooks) and manage them (manage_webhook: create/update/delete/test/
4
+ * rotate_secret). The read is always on; the mutations are safe writes.
5
5
  */
6
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.');
7
+ const webhookId = z.number().int().positive().optional().describe('The webhook id.');
11
8
  const listWebhooks = {
12
9
  name: 'list_webhooks',
13
10
  toolset: 'webhooks',
14
11
  gate: 'read',
15
12
  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.',
13
+ description: "Read your webhook subscriptions. `view: 'config'` (the default) lists the webhook subscriptions configured on your account — each with its URL, subscribed events and active state — or retrieves one subscription when `webhook_id` is given. " +
14
+ "`view: 'logs'` retrieves the recent delivery log for a webhook (`webhook_id` required) — attempts, response codes and outcomes — to debug why events did or did not reach your endpoint.",
57
15
  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,
16
+ webhook_id: webhookId,
17
+ view: z
18
+ .enum(['config', 'logs'])
19
+ .optional()
20
+ .describe('config (default) reads subscriptions; logs reads a webhook’s delivery log.'),
21
+ },
22
+ annotations: { readOnlyHint: true },
23
+ handler: (args, { client }) => {
24
+ const view = args.view ?? 'config';
25
+ if (view === 'logs') {
26
+ if (args.webhook_id === undefined) {
27
+ return Promise.resolve({
28
+ error: {
29
+ code: 'INVALID_SELECTOR',
30
+ message: "view 'logs' requires `webhook_id` — the webhook whose delivery log to read.",
31
+ status: 0,
32
+ },
33
+ });
34
+ }
35
+ return client.get(`/webhooks/${args.webhook_id}/logs`);
36
+ }
37
+ if (args.webhook_id !== undefined) {
38
+ return client.get(`/webhooks/${args.webhook_id}`);
39
+ }
40
+ return client.get('/webhooks');
61
41
  },
62
- annotations: {},
63
- handler: (args, { client }) => client.post('/webhooks', { name: args.name, url: args.url, events: args.events }),
64
42
  };
65
- const updateWebhook = {
66
- name: 'update_webhook',
43
+ const manageWebhook = {
44
+ name: 'manage_webhook',
67
45
  toolset: 'webhooks',
68
46
  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).',
47
+ title: 'Manage a webhook',
48
+ description: 'Manage a webhook subscription. Pick ONE action with `action`: ' +
49
+ '`create` — pass `fields` with `name`, `url` (the HTTPS endpoint receiving deliveries) and `events` (the event subscription object — see list_reference_data type webhook_event_types); the API returns a signing secret ONCE on creation — store it to verify incoming payloads. ' +
50
+ '`update` — supply only the fields to change in `fields`: name, url, events, or is_active (false pauses deliveries). ' +
51
+ '`delete` — permanently removes the subscription; it stops receiving events. ' +
52
+ '`test` — sends a test event to confirm reachability and signature verification; safe to repeat. ' +
53
+ '`rotate_secret` — generates and returns a new signing secret — WARNING: the old secret stops working immediately; update your endpoint right away or deliveries will fail verification. ' +
54
+ '`webhook_id` is required for every action except create.',
71
55
  inputShape: {
56
+ action: z
57
+ .enum(['create', 'update', 'delete', 'test', 'rotate_secret'])
58
+ .describe('Which action.'),
72
59
  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.'),
60
+ fields: z
61
+ .record(z.string(), z.unknown())
62
+ .optional()
63
+ .describe('The webhook attributes (create: name, url, events; update: any of those plus is_active), forwarded verbatim.'),
77
64
  },
78
- annotations: {},
65
+ annotations: { destructiveHint: true },
79
66
  handler: (args, { client }) => {
80
- const { webhook_id, ...body } = args;
81
- return client.patch(`/webhooks/${webhook_id}`, body);
67
+ const action = args.action;
68
+ if (action === 'create') {
69
+ return client.post('/webhooks', args.fields);
70
+ }
71
+ if (args.webhook_id === undefined) {
72
+ return Promise.resolve({
73
+ error: {
74
+ code: 'INVALID_SELECTOR',
75
+ message: `action '${action}' requires \`webhook_id\` — the webhook to act on. Only action 'create' works without one.`,
76
+ status: 0,
77
+ },
78
+ });
79
+ }
80
+ switch (action) {
81
+ case 'update':
82
+ return client.patch(`/webhooks/${args.webhook_id}`, args.fields);
83
+ case 'delete':
84
+ return client.delete(`/webhooks/${args.webhook_id}`);
85
+ case 'test':
86
+ return client.post(`/webhooks/${args.webhook_id}/test`);
87
+ default: // rotate_secret
88
+ return client.post(`/webhooks/${args.webhook_id}/regenerate-secret`);
89
+ }
82
90
  },
83
91
  };
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
- ];
92
+ export const webhookTools = [listWebhooks, manageWebhook];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labelgrid/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.labelgrid/labelgrid-mcp",
5
5
  "description": "Official LabelGrid MCP server — connect your AI client to your LabelGrid account",
6
6
  "type": "module",
@@ -12,32 +12,23 @@
12
12
  "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE", "server.json"],
13
13
  "scripts": {
14
14
  "build": "tsc",
15
- "build:mcpb": "npm run build && node scripts/build-mcpb.mjs",
16
15
  "start": "node dist/index.js",
17
16
  "dev": "tsc --watch",
18
17
  "test": "vitest run --exclude 'test/contract/**'",
19
- "test:contract": "vitest run test/contract",
20
- "lint": "biome check .",
21
- "leak-guard": "node scripts/leak-guard.mjs",
22
- "check-coverage": "node scripts/check-api-coverage.mjs",
23
- "gen-docs": "node scripts/gen-tool-docs.mjs"
18
+ "test:contract": "vitest run test/contract"
24
19
  },
25
20
  "repository": {
26
21
  "type": "git",
27
- "url": "git+https://github.com/labelgrid/labelgrid-mcp.git"
22
+ "url": "git+https://github.com/labelgrid/labelgrid-mcp.git",
23
+ "directory": "packages/mcp"
28
24
  },
29
25
  "license": "MIT",
30
26
  "engines": {
31
27
  "node": ">=20"
32
28
  },
33
29
  "dependencies": {
30
+ "@labelgrid/core": "0.1.0",
34
31
  "@modelcontextprotocol/sdk": "^1.12.0",
35
32
  "zod": "^3.24.0"
36
- },
37
- "devDependencies": {
38
- "@biomejs/biome": "^1.9.0",
39
- "@types/node": "^22.0.0",
40
- "typescript": "^5.7.0",
41
- "vitest": "^3.0.0"
42
33
  }
43
34
  }
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.labelgrid/labelgrid-mcp",
4
4
  "description": "Official LabelGrid MCP server — manage your music catalog, releases, analytics and distribution.",
5
- "version": "0.2.2",
5
+ "version": "0.3.1",
6
6
  "websiteUrl": "https://labelgrid.com",
7
7
  "repository": {
8
8
  "url": "https://github.com/labelgrid/labelgrid-mcp",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@labelgrid/mcp",
15
- "version": "0.2.2",
15
+ "version": "0.3.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -50,7 +50,7 @@
50
50
  },
51
51
  {
52
52
  "name": "LABELGRID_TOOLSETS",
53
- "description": "Comma-separated subset of toolsets to expose (identity, reference, catalog, releases, review, analytics, accounting, delivery, webhooks, distribution). Defaults to all.",
53
+ "description": "Comma-separated subset of toolsets to expose (account, reference, catalog, releases, insights, finance, webhooks, distribution; pre-0.3.0 names are accepted as aliases). Defaults to all except webhooks, which must be named explicitly to enable.",
54
54
  "isRequired": false,
55
55
  "format": "string"
56
56
  },
@@ -1,33 +0,0 @@
1
- /**
2
- * Shared file content-type inference and the upload extension allow-list guard.
3
- *
4
- * CONTENT_TYPES (moved here from upload.ts) infers a best-effort MIME type from
5
- * a file extension — used for the presigned PUT and the multipart Blob.
6
- * assertAllowedExtension is the per-tool guard: each file-accepting tool
7
- * declares exactly which extensions it accepts, and the guard rejects anything
8
- * else BEFORE the file is read or any HTTP call is made, so an upload tool can
9
- * never be pointed at an arbitrary local file.
10
- */
11
- import type { ApiError } from './http.js';
12
- /** Best-effort Content-Type inferred from a file extension. */
13
- export declare const CONTENT_TYPES: Record<string, string>;
14
- /** Best-effort Content-Type for a file path (default application/octet-stream). */
15
- export declare function contentType(filePath: string): string;
16
- /**
17
- * Rejects a file whose extension is not in `allowed` (case-insensitive), before
18
- * any read or HTTP call, and resolves the path to its real target. The supplied
19
- * path's extension is checked first (the fast path); then the path is resolved
20
- * with realpathSync and the REAL target's extension is checked too, so a symlink
21
- * named `cover.jpg` that points at an arbitrary local file cannot slip past the
22
- * guard. On success it returns `{ realPath }` — the resolved canonical path,
23
- * which the caller MUST use as the path it reads/uploads (never the original
24
- * argument), so a symlink retargeted after validation cannot redirect the read
25
- * (the resolved target is what gets uploaded). On failure it returns `{ error }`
26
- * — a structured FILE_TYPE_NOT_ALLOWED, or FILE_NOT_FOUND if the path does not
27
- * resolve.
28
- */
29
- export declare function assertAllowedExtension(filePath: string, allowed: string[]): {
30
- error: ApiError;
31
- } | {
32
- realPath: string;
33
- };
@@ -1,87 +0,0 @@
1
- /**
2
- * Shared file content-type inference and the upload extension allow-list guard.
3
- *
4
- * CONTENT_TYPES (moved here from upload.ts) infers a best-effort MIME type from
5
- * a file extension — used for the presigned PUT and the multipart Blob.
6
- * assertAllowedExtension is the per-tool guard: each file-accepting tool
7
- * declares exactly which extensions it accepts, and the guard rejects anything
8
- * else BEFORE the file is read or any HTTP call is made, so an upload tool can
9
- * never be pointed at an arbitrary local file.
10
- */
11
- import { realpathSync } from 'node:fs';
12
- import { extname } from 'node:path';
13
- /** Best-effort Content-Type inferred from a file extension. */
14
- export const CONTENT_TYPES = {
15
- '.wav': 'audio/wav',
16
- '.flac': 'audio/flac',
17
- '.aif': 'audio/aiff',
18
- '.aiff': 'audio/aiff',
19
- '.mp3': 'audio/mpeg',
20
- '.lrc': 'text/plain',
21
- '.txt': 'text/plain',
22
- '.jpg': 'image/jpeg',
23
- '.jpeg': 'image/jpeg',
24
- '.png': 'image/png',
25
- '.webp': 'image/webp',
26
- '.tif': 'image/tiff',
27
- '.tiff': 'image/tiff',
28
- '.pdf': 'application/pdf',
29
- '.mp4': 'video/mp4',
30
- '.mov': 'video/quicktime',
31
- };
32
- /** Best-effort Content-Type for a file path (default application/octet-stream). */
33
- export function contentType(filePath) {
34
- return CONTENT_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
35
- }
36
- /**
37
- * Rejects a file whose extension is not in `allowed` (case-insensitive), before
38
- * any read or HTTP call, and resolves the path to its real target. The supplied
39
- * path's extension is checked first (the fast path); then the path is resolved
40
- * with realpathSync and the REAL target's extension is checked too, so a symlink
41
- * named `cover.jpg` that points at an arbitrary local file cannot slip past the
42
- * guard. On success it returns `{ realPath }` — the resolved canonical path,
43
- * which the caller MUST use as the path it reads/uploads (never the original
44
- * argument), so a symlink retargeted after validation cannot redirect the read
45
- * (the resolved target is what gets uploaded). On failure it returns `{ error }`
46
- * — a structured FILE_TYPE_NOT_ALLOWED, or FILE_NOT_FOUND if the path does not
47
- * resolve.
48
- */
49
- export function assertAllowedExtension(filePath, allowed) {
50
- const isAllowed = (candidate) => allowed.some((a) => a.toLowerCase() === candidate);
51
- const ext = extname(filePath).toLowerCase();
52
- // Fast path: reject a plainly-disallowed extension before touching the disk.
53
- if (!isAllowed(ext)) {
54
- return {
55
- error: {
56
- code: 'FILE_TYPE_NOT_ALLOWED',
57
- message: `This tool only accepts ${allowed.join(', ')} files (got "${ext || 'no extension'}").`,
58
- status: 0,
59
- },
60
- };
61
- }
62
- // The supplied name is allowed; resolve symlinks and re-check the real target.
63
- let realPath;
64
- try {
65
- realPath = realpathSync(filePath);
66
- }
67
- catch {
68
- return {
69
- error: {
70
- code: 'FILE_NOT_FOUND',
71
- message: `No readable file at ${filePath}.`,
72
- status: 0,
73
- },
74
- };
75
- }
76
- const realExt = extname(realPath).toLowerCase();
77
- if (!isAllowed(realExt)) {
78
- return {
79
- error: {
80
- code: 'FILE_TYPE_NOT_ALLOWED',
81
- message: `The file resolves to a "${realExt || 'no extension'}" file; this tool only accepts ${allowed.join(', ')}.`,
82
- status: 0,
83
- },
84
- };
85
- }
86
- return { realPath };
87
- }