@likerts/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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Likerts contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Likerts MCP server
2
+
3
+ Operate the free, MIT-licensed Likerts survey platform from an MCP client. The server exposes the same typed operations and authorization boundaries as the HTTP API and CLI.
4
+
5
+ The hosted server is listed in the [official MCP registry](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.crosstabs%2Flikerts/versions/latest) as `io.github.crosstabs/likerts`. Configure your workspace ID and scoped service credential in your MCP client. The listing does not make workspace data public.
6
+
7
+ ## Install
8
+
9
+ Install a published version from npm:
10
+
11
+ ```sh
12
+ npm install --global @likerts/mcp
13
+ ```
14
+
15
+ Alternatively, download the MCP tarball from the [community release](https://github.com/crosstabs/likerts/releases) and run `npm install --global /absolute/path/to/the-mcp-package.tgz`. Node.js 22 or newer is required.
16
+
17
+ Configure your MCP client to run `likerts-mcp` over stdio. Set `LIKERTS_API_URL` to your API origin and supply a scoped `LIKERTS_TOKEN` through your environment or secret manager. The local API defaults to `http://127.0.0.1:8080`. Collection operations use `LIKERTS_COLLECTION_TOKEN` separately; keep management credentials out of embedded apps and tool arguments.
18
+
19
+ For a source checkout:
20
+
21
+ ```sh
22
+ npm ci --prefix tools/mcp
23
+ npm run build --prefix tools/mcp
24
+ node tools/mcp/dist/main.js
25
+ ```
26
+
27
+ Launch the command from an MCP client; stdout is reserved for protocol messages. Authentication and permission checks remain enforced by your API.
28
+
29
+ ## Remote connection
30
+
31
+ The optional hosted endpoint is `https://likerts-mcp.onrender.com/mcp/{workspaceId}`. It requires the workspace's scoped service credential. Self-hosters use their own remote host.
32
+
33
+ [Setup for Codex and Claude](https://github.com/crosstabs/likerts/blob/main/tools/README.md) · [Quickstart](https://likerts.com/docs) · [API reference](https://likerts.com/docs/api)
package/dist/client.js ADDED
@@ -0,0 +1,76 @@
1
+ import { readContractResource } from './resources.js';
2
+ export const capabilities = readContractResource('capabilities.json');
3
+ export class LikertsHttpError extends Error {
4
+ operation;
5
+ status;
6
+ constructor(operation, status) {
7
+ super(`Likerts ${operation} failed (HTTP ${status})`);
8
+ this.operation = operation;
9
+ this.status = status;
10
+ }
11
+ }
12
+ export class LikertsClient {
13
+ managementToken;
14
+ collectionToken;
15
+ transport;
16
+ workspaceId;
17
+ base;
18
+ constructor(baseUrl, managementToken, collectionToken, transport = fetch, workspaceId) {
19
+ this.managementToken = managementToken;
20
+ this.collectionToken = collectionToken;
21
+ this.transport = transport;
22
+ this.workspaceId = workspaceId;
23
+ try {
24
+ this.base = new URL(baseUrl);
25
+ }
26
+ catch {
27
+ throw new Error('LIKERTS_API_URL must be a valid origin');
28
+ }
29
+ if (this.base.username || this.base.password || this.base.search || this.base.hash || this.base.pathname !== '/')
30
+ throw new Error('LIKERTS_API_URL must be an origin without credentials, path, query or fragment');
31
+ if (this.base.protocol !== 'https:' && !(this.base.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(this.base.hostname)))
32
+ throw new Error('HTTPS is required except on loopback');
33
+ }
34
+ async call(name, input) {
35
+ const operation = capabilities.find(c => c.name === name);
36
+ if (!operation)
37
+ throw new Error('Unknown capability');
38
+ const token = operation.auth === 'management' ? this.managementToken : this.collectionToken;
39
+ if (!token)
40
+ throw new Error(`Missing ${operation.auth} credential`);
41
+ let path = operation.path;
42
+ const body = { ...input };
43
+ if (path.includes('{id}')) {
44
+ if (typeof input.id !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(input.id))
45
+ throw new Error('Invalid resource ID');
46
+ path = path.replace('{id}', encodeURIComponent(input.id));
47
+ delete body.id;
48
+ }
49
+ const url = new URL(path, this.base);
50
+ if (operation.method === 'GET') {
51
+ for (const [key, value] of Object.entries(body)) {
52
+ if (value === undefined)
53
+ continue;
54
+ if (!['string', 'number', 'boolean'].includes(typeof value))
55
+ throw new Error('Invalid query input');
56
+ url.searchParams.set(key, String(value));
57
+ }
58
+ }
59
+ const response = await this.transport(url, {
60
+ method: operation.method, redirect: 'error', signal: AbortSignal.timeout(30_000),
61
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', ...(operation.auth === 'management' && this.workspaceId ? { 'x-likerts-workspace': this.workspaceId } : {}) },
62
+ ...(operation.method === 'GET' ? {} : { body: JSON.stringify(body) })
63
+ });
64
+ // Do not echo upstream bodies: malformed servers may return credentials or HTML.
65
+ if (!response.ok)
66
+ throw new LikertsHttpError(name, response.status);
67
+ if (response.status === 204)
68
+ return {};
69
+ try {
70
+ return await response.json();
71
+ }
72
+ catch {
73
+ throw new Error('Server returned invalid JSON');
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,68 @@
1
+ import { readContractResource } from './resources.js';
2
+ import { createRequire } from 'node:module';
3
+ import { capabilities } from './client.js';
4
+ const spec = readContractResource('openapi.json');
5
+ const Ajv = createRequire(import.meta.url)('ajv/dist/2020').default;
6
+ const ajv = new Ajv({ strict: false, allErrors: true });
7
+ ajv.addFormat('uuid', /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
8
+ ajv.addFormat('date-time', (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value)));
9
+ ajv.addFormat('uri', (value) => { try {
10
+ new URL(value);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ } });
16
+ // Inline local references so MCP clients need no external schema resolver.
17
+ function inline(value, ancestors = []) {
18
+ if (Array.isArray(value))
19
+ return value.map(item => inline(item, ancestors));
20
+ if (value && typeof value === 'object') {
21
+ if (value.$ref) {
22
+ if (!value.$ref.startsWith('#/components/schemas/') || ancestors.includes(value.$ref))
23
+ throw new Error('Unsupported or recursive contract reference');
24
+ const schema = spec.components.schemas[value.$ref.split('/').at(-1)];
25
+ if (!schema)
26
+ throw new Error('Missing contract schema');
27
+ return inline(schema, [...ancestors, value.$ref]);
28
+ }
29
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, inline(item, ancestors)]));
30
+ }
31
+ return value;
32
+ }
33
+ export const operationContracts = new Map();
34
+ for (const capability of capabilities) {
35
+ const operation = spec.paths[capability.path]?.[capability.method.toLowerCase()];
36
+ if (operation?.operationId !== capability.name)
37
+ throw new Error(`Missing OpenAPI capability ${capability.name}`);
38
+ const body = inline(operation.requestBody?.content?.['application/json']?.schema ?? { type: 'object', properties: {}, additionalProperties: false });
39
+ if (body.type !== 'object')
40
+ throw new Error('Capability body must be an object');
41
+ const parameters = operation.parameters ?? [];
42
+ const properties = { ...body.properties };
43
+ const required = [...(body.required ?? [])];
44
+ for (const parameter of parameters) {
45
+ if (!['path', 'query'].includes(parameter.in))
46
+ continue;
47
+ if (parameter.name in properties)
48
+ throw new Error('Ambiguous body/path/query input field');
49
+ properties[parameter.name] = inline(parameter.schema);
50
+ if (parameter.required)
51
+ required.push(parameter.name);
52
+ }
53
+ const parameterCount = parameters.filter(p => ['path', 'query'].includes(p.in) && p.required).length;
54
+ const inputSchema = { ...body, type: 'object', properties, required, additionalProperties: false };
55
+ if (inputSchema.minProperties !== undefined)
56
+ inputSchema.minProperties += parameterCount;
57
+ if (inputSchema.maxProperties !== undefined)
58
+ inputSchema.maxProperties += parameterCount;
59
+ const successes = Object.entries(operation.responses).filter(([status]) => /^2\d\d$/.test(status));
60
+ if (successes.length !== 1)
61
+ throw new Error('Capability must have one explicit success response');
62
+ const [status, response] = successes[0];
63
+ const resultSchema = status === '204' ? { type: 'object', additionalProperties: false } : inline(response.content?.['application/json']?.schema);
64
+ if (!resultSchema)
65
+ throw new Error('Missing successful output schema');
66
+ const outputSchema = { type: 'object', properties: { result: resultSchema }, required: ['result'], additionalProperties: false };
67
+ operationContracts.set(capability.name, { inputSchema, outputSchema, validateInput: ajv.compile(inputSchema), validateOutput: ajv.compile(outputSchema) });
68
+ }
@@ -0,0 +1,266 @@
1
+ [
2
+ {
3
+ "name": "surveys_create",
4
+ "method": "POST",
5
+ "path": "/v1/surveys",
6
+ "description": "Create a survey draft idempotently; identical retries return the original draft.",
7
+ "input": "survey",
8
+ "auth": "management"
9
+ },
10
+ {
11
+ "name": "surveys_list",
12
+ "method": "GET",
13
+ "path": "/v1/surveys",
14
+ "description": "List workspace surveys.",
15
+ "input": "empty",
16
+ "auth": "management"
17
+ },
18
+ {
19
+ "name": "surveys_update",
20
+ "method": "PUT",
21
+ "path": "/v1/surveys/{id}",
22
+ "description": "Update a draft using its current revision.",
23
+ "input": "update",
24
+ "auth": "management"
25
+ },
26
+ {
27
+ "name": "surveys_publish",
28
+ "method": "POST",
29
+ "path": "/v1/surveys/{id}/publish",
30
+ "description": "Publish an immutable version only when every customer-declared SDK installation supports its schema version.",
31
+ "input": "publish",
32
+ "auth": "management"
33
+ },
34
+ {
35
+ "name": "collections_create",
36
+ "method": "POST",
37
+ "path": "/v1/collections",
38
+ "description": "Create an immutable embedded collection only when every declared SDK installation supports the bound schema version. Identical retries return the original submission credential; handle it as a secret.",
39
+ "input": "collection",
40
+ "auth": "management"
41
+ },
42
+ {
43
+ "name": "collections_update",
44
+ "method": "PATCH",
45
+ "path": "/v1/collections/{id}",
46
+ "description": "Open or close collection acceptance, or irreversibly revoke its credential.",
47
+ "input": "acceptance",
48
+ "auth": "management"
49
+ },
50
+ {
51
+ "name": "collections_security_update",
52
+ "method": "PUT",
53
+ "path": "/v1/collections/{id}/security",
54
+ "description": "Configure exact browser origins and a per-collection request rate. Origins are browser policy, not authentication.",
55
+ "input": "collection_security",
56
+ "auth": "management"
57
+ },
58
+ {
59
+ "name": "collections_get",
60
+ "method": "GET",
61
+ "path": "/v1/collections/{id}",
62
+ "description": "Fetch the immutable respondent configuration using a collection credential.",
63
+ "input": "id",
64
+ "auth": "collection"
65
+ },
66
+ {
67
+ "name": "responses_submit",
68
+ "method": "POST",
69
+ "path": "/v1/collections/{id}/responses",
70
+ "description": "Submit a completed response. Accepted responses are counted for observability and are never metered or charged. Reuse the same idempotency key only for identical retries.",
71
+ "input": "submission",
72
+ "auth": "collection"
73
+ },
74
+ {
75
+ "name": "responses_list",
76
+ "method": "GET",
77
+ "path": "/v1/responses",
78
+ "description": "Retrieve one stable snapshot page of workspace responses, optionally filtered by collection and acceptance time. Continue with nextCursor.",
79
+ "input": "response_list",
80
+ "auth": "management"
81
+ },
82
+ {
83
+ "name": "responses_delete",
84
+ "method": "DELETE",
85
+ "path": "/v1/responses/{id}",
86
+ "description": "Erase one response's raw answers and metadata while preserving its minimal retry receipt and usage entry.",
87
+ "input": "id",
88
+ "auth": "management"
89
+ },
90
+ {
91
+ "name": "exports_create",
92
+ "method": "POST",
93
+ "path": "/v1/exports",
94
+ "description": "Create or idempotently retry a bounded asynchronous CSV or JSON response export.",
95
+ "input": "export",
96
+ "auth": "management"
97
+ },
98
+ {
99
+ "name": "exports_get",
100
+ "method": "GET",
101
+ "path": "/v1/exports/{id}",
102
+ "description": "Read an export job's status and stable schema manifest.",
103
+ "input": "id",
104
+ "auth": "management"
105
+ },
106
+ {
107
+ "name": "exports_download",
108
+ "method": "GET",
109
+ "path": "/v1/exports/{id}/download",
110
+ "description": "Download a ready export as authenticated base64 content before its 24-hour expiry.",
111
+ "input": "id",
112
+ "auth": "management"
113
+ },
114
+ {
115
+ "name": "exports_revoke",
116
+ "method": "DELETE",
117
+ "path": "/v1/exports/{id}",
118
+ "description": "Irreversibly revoke export access and remove its stored object.",
119
+ "input": "id",
120
+ "auth": "management"
121
+ },
122
+ {
123
+ "name": "retention_run",
124
+ "method": "POST",
125
+ "path": "/v1/retention",
126
+ "description": "Run one bounded raw-response and expired-export retention batch for the workspace.",
127
+ "input": "empty",
128
+ "auth": "management"
129
+ },
130
+ {
131
+ "name": "workspace_delete",
132
+ "method": "DELETE",
133
+ "path": "/v1/workspace",
134
+ "description": "Erase and tombstone the current workspace account, revoke capabilities and remove export objects.",
135
+ "input": "empty",
136
+ "auth": "management"
137
+ },
138
+ {
139
+ "name": "memberships_list",
140
+ "method": "GET",
141
+ "path": "/v1/memberships",
142
+ "description": "List current workspace memberships. Owner access is required.",
143
+ "input": "empty",
144
+ "auth": "management"
145
+ },
146
+ {
147
+ "name": "memberships_put",
148
+ "method": "PUT",
149
+ "path": "/v1/memberships",
150
+ "description": "Grant or replace a workspace membership role. Owner access is required.",
151
+ "input": "membership",
152
+ "auth": "management"
153
+ },
154
+ {
155
+ "name": "memberships_revoke",
156
+ "method": "POST",
157
+ "path": "/v1/memberships/revoke",
158
+ "description": "Revoke a workspace membership immediately. Owner access is required.",
159
+ "input": "subject",
160
+ "auth": "management"
161
+ },
162
+ {
163
+ "name": "service_credentials_list",
164
+ "method": "GET",
165
+ "path": "/v1/service-credentials",
166
+ "description": "List service credential metadata without secret values. Owner access is required.",
167
+ "input": "empty",
168
+ "auth": "management"
169
+ },
170
+ {
171
+ "name": "service_credentials_create",
172
+ "method": "POST",
173
+ "path": "/v1/service-credentials",
174
+ "description": "Issue a scoped service credential. Its token is returned exactly once.",
175
+ "input": "service_credential",
176
+ "auth": "management"
177
+ },
178
+ {
179
+ "name": "service_credentials_revoke",
180
+ "method": "DELETE",
181
+ "path": "/v1/service-credentials/{id}",
182
+ "description": "Irreversibly revoke a service credential.",
183
+ "input": "id",
184
+ "auth": "management"
185
+ },
186
+ {
187
+ "name": "oauth_grants_create",
188
+ "method": "POST",
189
+ "path": "/v1/oauth-grants",
190
+ "description": "Persist consent as a tenant, subject, client, audience and scope-bound OAuth grant.",
191
+ "input": "oauth_grant",
192
+ "auth": "management"
193
+ },
194
+ {
195
+ "name": "oauth_grants_revoke",
196
+ "method": "DELETE",
197
+ "path": "/v1/oauth-grants/{id}",
198
+ "description": "Irreversibly revoke an OAuth grant for subsequent requests.",
199
+ "input": "id",
200
+ "auth": "management"
201
+ },
202
+ {
203
+ "name": "usage_get",
204
+ "method": "GET",
205
+ "path": "/v1/usage",
206
+ "description": "Retrieve accepted-response usage.",
207
+ "input": "empty",
208
+ "auth": "management"
209
+ },
210
+ {
211
+ "name": "webhook_endpoints_create",
212
+ "method": "POST",
213
+ "path": "/v1/webhook-endpoints",
214
+ "description": "Creates disabled. Install the returned signing secret on your receiver, then explicitly enable. An identical idempotency retry reconstructs the original generation.",
215
+ "input": "WebhookEndpointInput",
216
+ "auth": "management"
217
+ },
218
+ {
219
+ "name": "webhook_endpoints_list",
220
+ "method": "GET",
221
+ "path": "/v1/webhook-endpoints",
222
+ "description": "Lists up to 100 workspace endpoints, including revoked entries. Signing secrets are never listed.",
223
+ "input": "empty",
224
+ "auth": "management"
225
+ },
226
+ {
227
+ "name": "webhook_endpoints_update",
228
+ "method": "PATCH",
229
+ "path": "/v1/webhook-endpoints/{id}",
230
+ "description": "Enable only after configuring verification. Use enabled:false to pause, or revoke:true to permanently cancel queued deliveries; an in-flight HTTP request cannot be recalled.",
231
+ "input": "WebhookEndpointUpdate",
232
+ "auth": "management"
233
+ },
234
+ {
235
+ "name": "webhook_endpoints_rotate",
236
+ "method": "POST",
237
+ "path": "/v1/webhook-endpoints/{id}/rotate-key",
238
+ "description": "Install the new generation and retain the old receiver secret for five minutes. New claims use the new generation; identical rotation retries return the original protected result.",
239
+ "input": "WebhookOperationInput",
240
+ "auth": "management"
241
+ },
242
+ {
243
+ "name": "webhook_deliveries_list",
244
+ "method": "GET",
245
+ "path": "/v1/webhook-deliveries",
246
+ "description": "Inspect delivery status. Continue with after equal to the last returned UUID for a stable ordering of existing rows; this operational list is not a response snapshot.",
247
+ "input": "empty",
248
+ "auth": "management"
249
+ },
250
+ {
251
+ "name": "webhook_deliveries_get",
252
+ "method": "GET",
253
+ "path": "/v1/webhook-deliveries/{id}",
254
+ "description": "Read attempts, retry time and fixed failure code. Receiver bodies and survey answers are never exposed through delivery status.",
255
+ "input": "empty",
256
+ "auth": "management"
257
+ },
258
+ {
259
+ "name": "webhook_deliveries_replay",
260
+ "method": "POST",
261
+ "path": "/v1/webhook-deliveries/{id}/replay",
262
+ "description": "Requeue a completed or failed delivery before seven-day expiry, up to three times. The event ID remains unchanged; receivers deduplicate business processing by that ID.",
263
+ "input": "WebhookOperationInput",
264
+ "auth": "management"
265
+ }
266
+ ]