@emotionlogic/mcp 1.0.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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @emotionlogic/mcp
2
+
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io/) server for the EmotionLogic API. It exposes AppTone and FeelGPT operations to MCP clients such as Cursor and Claude Desktop.
4
+
5
+ ## Get API credentials
6
+
7
+ The server authenticates with **HTTP Basic Auth**. Use an API **username** and **password** from EmotionLogic Hub — not the email of your Hub login.
8
+
9
+ 1. Create an EmotionLogic Hub account at [app.emotionlogic.ai](https://app.emotionlogic.ai/).
10
+ 2. Sign in and open **Analyze Now API Keys** (`/analyze-now-api/api-keys`).
11
+ 3. Create an API user: give it a name and set a password.
12
+ 4. Copy the generated **username** (a UUID) and the password you set.
13
+ 5. Set them as `EMOTIONLOGIC_API_USERNAME` and `EMOTIONLOGIC_API_PASSWORD`.
14
+
15
+ The API user needs `apptone_read` for AppTone tools and `feelgpt_app_usage` for FeelGPT tools.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -g @emotionlogic/mcp
21
+ ```
22
+
23
+ Or run without a global install:
24
+
25
+ ```bash
26
+ npx @emotionlogic/mcp
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ | Variable | Required | Description |
32
+ | --- | --- | --- |
33
+ | `EMOTIONLOGIC_API_USERNAME` | Yes | API username from EmotionLogic Hub |
34
+ | `EMOTIONLOGIC_API_PASSWORD` | Yes | API user password |
35
+ | `EMOTIONLOGIC_API_BASE_URL` | No | Defaults to `https://api.emotionlogic.ai/apigateway-service` |
36
+
37
+ ### Cursor
38
+
39
+ Add the server to `.cursor/mcp.json`:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "emotionlogic": {
45
+ "command": "npx",
46
+ "args": ["-y", "@emotionlogic/mcp"],
47
+ "env": {
48
+ "EMOTIONLOGIC_API_USERNAME": "your-api-username",
49
+ "EMOTIONLOGIC_API_PASSWORD": "your-api-password"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Claude Desktop
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "emotionlogic": {
62
+ "command": "npx",
63
+ "args": ["-y", "@emotionlogic/mcp"],
64
+ "env": {
65
+ "EMOTIONLOGIC_API_USERNAME": "your-api-username",
66
+ "EMOTIONLOGIC_API_PASSWORD": "your-api-password"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Tools
74
+
75
+ ### AppTone
76
+
77
+ - `apptone_list_questionnaires`
78
+ - `apptone_send_to_customer`
79
+ - `apptone_get_analysis_result`
80
+ - `apptone_cancel_report`
81
+ - `apptone_download_pdf`
82
+ - `apptone_download_audio`
83
+
84
+ ### FeelGPT
85
+
86
+ - `feelgpt_list_advisors`
87
+ - `feelgpt_analyze`
88
+
89
+ ### Documentation
90
+
91
+ - `emotionlogic_get_api_reference` — OpenAPI contract or a single operation. No credentials required.
92
+
93
+ The server also exposes the OpenAPI document as the resource `emotionlogic://api/openapi.json` and a prompt for generating integration code.
94
+
95
+ ## Local development
96
+
97
+ ```bash
98
+ cd projects/emlo/backend/apiGatewayService/packages/mcp
99
+ npm install
100
+ npm test
101
+ ```
102
+
103
+ Copy `.env.example` and export the variables before starting:
104
+
105
+ ```bash
106
+ export EMOTIONLOGIC_API_USERNAME=
107
+ export EMOTIONLOGIC_API_PASSWORD=
108
+ npm start
109
+ ```
110
+
111
+ ## Publishing to npm
112
+
113
+ Publish to the [`@emotionlogic`](https://www.npmjs.com/settings/emotionlogic/packages) organization:
114
+
115
+ ```bash
116
+ npm login
117
+ npm test
118
+ npm publish --access public
119
+ ```
120
+
121
+ ## License
122
+
123
+ UNLICENSED
package/client.js ADDED
@@ -0,0 +1,141 @@
1
+ const fs = require("node:fs");
2
+ const axios = require("axios");
3
+ const FormData = require("form-data");
4
+
5
+ const DEFAULT_BASE_URL = "https://api.emotionlogic.ai/apigateway-service";
6
+
7
+ class EmotionLogicRequestError extends Error {
8
+ constructor(message, details = {}) {
9
+ super(message);
10
+ this.name = "EmotionLogicRequestError";
11
+ this.status = details.status;
12
+ this.data = details.data;
13
+ }
14
+ }
15
+
16
+ function getClient() {
17
+ const username = process.env.EMOTIONLOGIC_API_USERNAME;
18
+ const password = process.env.EMOTIONLOGIC_API_PASSWORD;
19
+
20
+ if (!username || !password) {
21
+ throw new EmotionLogicRequestError(
22
+ "EMOTIONLOGIC_API_USERNAME and EMOTIONLOGIC_API_PASSWORD must be configured."
23
+ );
24
+ }
25
+
26
+ return axios.create({
27
+ baseURL: (process.env.EMOTIONLOGIC_API_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, ""),
28
+ auth: {
29
+ username,
30
+ password,
31
+ },
32
+ });
33
+ }
34
+
35
+ function normalizeAxiosError(error) {
36
+ if (error instanceof EmotionLogicRequestError) {
37
+ return error;
38
+ }
39
+
40
+ if (axios.isAxiosError(error)) {
41
+ const status = error.response?.status;
42
+ const message = status
43
+ ? `EmotionLogic API request failed with HTTP ${status}.`
44
+ : `EmotionLogic API request failed: ${error.message}`;
45
+
46
+ return new EmotionLogicRequestError(message, {
47
+ status,
48
+ data: error.response?.data,
49
+ });
50
+ }
51
+
52
+ return new EmotionLogicRequestError(error instanceof Error ? error.message : String(error));
53
+ }
54
+
55
+ function appendFormValue(form, key, value) {
56
+ if (value === undefined || value === null) {
57
+ return;
58
+ }
59
+
60
+ if (Array.isArray(value)) {
61
+ value.forEach((item) => appendFormValue(form, key, item));
62
+ return;
63
+ }
64
+
65
+ form.append(key, typeof value === "boolean" ? String(value) : value);
66
+ }
67
+
68
+ async function requestJson({method, path, query, body}) {
69
+ try {
70
+ const response = await getClient().request({
71
+ method,
72
+ url: path,
73
+ params: query,
74
+ data: body,
75
+ });
76
+
77
+ return response.data;
78
+ } catch (error) {
79
+ throw normalizeAxiosError(error);
80
+ }
81
+ }
82
+
83
+ async function requestMultipart({path, fields, filePath, fileField = "file"}) {
84
+ try {
85
+ const fileStats = await fs.promises.stat(filePath);
86
+ if (!fileStats.isFile()) {
87
+ throw new EmotionLogicRequestError(`File path is not a regular file: ${filePath}`);
88
+ }
89
+
90
+ const form = new FormData();
91
+ Object.entries(fields).forEach(([key, value]) => appendFormValue(form, key, value));
92
+ form.append(fileField, fs.createReadStream(filePath));
93
+
94
+ const response = await getClient().post(path, form, {
95
+ headers: form.getHeaders(),
96
+ maxBodyLength: Infinity,
97
+ });
98
+
99
+ return response.data;
100
+ } catch (error) {
101
+ if (error?.code === "ENOENT") {
102
+ throw new EmotionLogicRequestError(`File does not exist: ${filePath}`);
103
+ }
104
+ throw normalizeAxiosError(error);
105
+ }
106
+ }
107
+
108
+ function formatToolResult(data) {
109
+ const text = typeof data === "string" ? data : JSON.stringify(data ?? {}, null, 2);
110
+ return {
111
+ content: [{type: "text", text}],
112
+ };
113
+ }
114
+
115
+ function formatToolError(error) {
116
+ const normalized = normalizeAxiosError(error);
117
+ const payload = {
118
+ error: normalized.message,
119
+ ...(normalized.status ? {status: normalized.status} : {}),
120
+ ...(normalized.data !== undefined ? {details: normalized.data} : {}),
121
+ };
122
+
123
+ return {
124
+ content: [{type: "text", text: JSON.stringify(payload, null, 2)}],
125
+ isError: true,
126
+ };
127
+ }
128
+
129
+ async function runTool(request) {
130
+ try {
131
+ return formatToolResult(await request());
132
+ } catch (error) {
133
+ return formatToolError(error);
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ requestJson,
139
+ requestMultipart,
140
+ runTool,
141
+ };
@@ -0,0 +1,602 @@
1
+ const PRODUCTION_BASE_URL = "https://api.emotionlogic.ai/apigateway-service";
2
+
3
+ const OPERATION_IDS = [
4
+ "apptoneCancelReport",
5
+ "apptoneDownloadPdf",
6
+ "apptoneGetAnalysisResult",
7
+ "apptoneDownloadAudio",
8
+ "apptoneListQuestionnaires",
9
+ "apptoneSendToCustomer",
10
+ "feelgptListAdvisors",
11
+ "feelgptAnalyze",
12
+ ];
13
+
14
+ const reportIdParameter = {
15
+ name: "reportId",
16
+ in: "path",
17
+ required: true,
18
+ description: "External report identifier.",
19
+ schema: {type: "string", minLength: 1},
20
+ };
21
+
22
+ const questionnaireIdParameter = {
23
+ name: "questionnaireId",
24
+ in: "path",
25
+ required: true,
26
+ description: "External AppTone questionnaire identifier.",
27
+ schema: {type: "string", minLength: 1},
28
+ };
29
+
30
+ const appIdParameter = {
31
+ name: "appId",
32
+ in: "path",
33
+ required: true,
34
+ description: "FeelGPT advisor application identifier.",
35
+ schema: {type: "string", minLength: 1},
36
+ };
37
+
38
+ const standardErrorResponses = {
39
+ 400: {$ref: "#/components/responses/BadRequest"},
40
+ 401: {$ref: "#/components/responses/Unauthorized"},
41
+ 403: {$ref: "#/components/responses/Forbidden"},
42
+ 500: {$ref: "#/components/responses/InternalError"},
43
+ };
44
+
45
+ const callbackProperties = {
46
+ statusCallbackUrl: {
47
+ type: "string",
48
+ format: "uri",
49
+ description: "HTTPS endpoint that receives the asynchronous result.",
50
+ examples: ["https://example.com/webhooks/emotionlogic"],
51
+ },
52
+ statusCallbackEmail: {
53
+ type: "string",
54
+ format: "email",
55
+ description: "Email address that receives the asynchronous result.",
56
+ examples: ["results@example.com"],
57
+ },
58
+ };
59
+
60
+ const OPENAPI_DOCUMENT = {
61
+ openapi: "3.1.0",
62
+ info: {
63
+ title: "EmotionLogic API",
64
+ version: "1.0.0",
65
+ description: [
66
+ "Public API for AppTone questionnaire reports and FeelGPT advisor analysis.",
67
+ "All operations require HTTP Basic authentication with an API username and password.",
68
+ "Create credentials in EmotionLogic Hub at https://app.emotionlogic.ai/ under Analyze Now API Keys.",
69
+ "Long-running operations return a report ID and deliver their final result to the supplied callback URL or email.",
70
+ ].join(" "),
71
+ },
72
+ servers: [
73
+ {
74
+ url: PRODUCTION_BASE_URL,
75
+ description: "EmotionLogic production API",
76
+ },
77
+ ],
78
+ security: [{basicAuth: []}],
79
+ tags: [
80
+ {name: "AppTone", description: "Questionnaires and emotion-analysis reports."},
81
+ {name: "FeelGPT", description: "Advisor discovery and media analysis."},
82
+ ],
83
+ "x-integration-guidance": {
84
+ authentication: "Use HTTP Basic Auth. Keep the API username and password in environment variables or a secret manager. Create credentials at https://app.emotionlogic.ai/ under Analyze Now API Keys.",
85
+ contentTypes: {
86
+ json: "Use application/json for JSON request bodies.",
87
+ multipart: "Use multipart/form-data for FeelGPT media uploads. The file field name is file.",
88
+ },
89
+ callbacks: [
90
+ "sendToCustomer and analyze are asynchronous and immediately return a reportId.",
91
+ "Provide statusCallbackUrl or statusCallbackEmail to receive the final result.",
92
+ "downloadPdf and downloadAudio require statusCallbackUrl.",
93
+ "When encryptionKey is supplied, the callback payload may be encrypted by the service.",
94
+ ],
95
+ errors: "Treat non-2xx responses as failures and inspect errorCode and error.",
96
+ },
97
+ paths: {
98
+ "/apptone/v1/reports/{reportId}/cancel": {
99
+ post: {
100
+ operationId: "apptoneCancelReport",
101
+ tags: ["AppTone"],
102
+ summary: "Cancel an AppTone report",
103
+ description: "Cancels or pulls back a questionnaire report.",
104
+ "x-required-permission": "apptone_read",
105
+ parameters: [reportIdParameter],
106
+ responses: {
107
+ 200: {
108
+ description: "The report was cancelled.",
109
+ content: {
110
+ "application/json": {
111
+ schema: {type: "object", additionalProperties: true},
112
+ example: {},
113
+ },
114
+ },
115
+ },
116
+ 404: {$ref: "#/components/responses/NotFound"},
117
+ ...standardErrorResponses,
118
+ },
119
+ "x-codeSamples": [
120
+ {
121
+ lang: "curl",
122
+ source: `curl -X POST -u "$EMOTIONLOGIC_API_USERNAME:$EMOTIONLOGIC_API_PASSWORD" "${PRODUCTION_BASE_URL}/apptone/v1/reports/REPORT_ID/cancel"`,
123
+ },
124
+ ],
125
+ },
126
+ },
127
+ "/apptone/v1/reports/{reportId}/downloadPdf": {
128
+ post: {
129
+ operationId: "apptoneDownloadPdf",
130
+ tags: ["AppTone"],
131
+ summary: "Request an AppTone report PDF",
132
+ description: "Generates the PDF and delivers the result to statusCallbackUrl.",
133
+ "x-required-permission": "apptone_read",
134
+ parameters: [reportIdParameter],
135
+ requestBody: {
136
+ required: true,
137
+ content: {
138
+ "application/json": {
139
+ schema: {
140
+ type: "object",
141
+ required: ["statusCallbackUrl"],
142
+ properties: {
143
+ statusCallbackUrl: callbackProperties.statusCallbackUrl,
144
+ encryptionKey: {
145
+ type: ["string", "null"],
146
+ description: "Optional key used to encrypt callback data.",
147
+ },
148
+ },
149
+ additionalProperties: false,
150
+ },
151
+ example: {
152
+ statusCallbackUrl: "https://example.com/webhooks/emotionlogic",
153
+ },
154
+ },
155
+ },
156
+ },
157
+ responses: {
158
+ 200: {$ref: "#/components/responses/DownloadAccepted"},
159
+ 404: {$ref: "#/components/responses/NotFound"},
160
+ ...standardErrorResponses,
161
+ },
162
+ },
163
+ },
164
+ "/apptone/v1/reports/{reportId}/getAnalysisResult": {
165
+ get: {
166
+ operationId: "apptoneGetAnalysisResult",
167
+ tags: ["AppTone"],
168
+ summary: "Get an AppTone analysis result",
169
+ description: "Returns the available analysis data for a report.",
170
+ "x-required-permission": "apptone_read",
171
+ parameters: [reportIdParameter],
172
+ responses: {
173
+ 200: {
174
+ description: "Report analysis data.",
175
+ content: {
176
+ "application/json": {
177
+ schema: {
178
+ type: "object",
179
+ additionalProperties: true,
180
+ description: "The analysis structure depends on the questionnaire.",
181
+ },
182
+ },
183
+ },
184
+ },
185
+ 404: {$ref: "#/components/responses/NotFound"},
186
+ ...standardErrorResponses,
187
+ },
188
+ },
189
+ },
190
+ "/apptone/v1/reports/{reportId}/downloadAudio": {
191
+ post: {
192
+ operationId: "apptoneDownloadAudio",
193
+ tags: ["AppTone"],
194
+ summary: "Request AppTone report audio",
195
+ description: "Collects the report audio and delivers the result to statusCallbackUrl.",
196
+ "x-required-permission": "apptone_read",
197
+ parameters: [reportIdParameter],
198
+ requestBody: {
199
+ required: true,
200
+ content: {
201
+ "application/json": {
202
+ schema: {
203
+ type: "object",
204
+ required: ["statusCallbackUrl"],
205
+ properties: {
206
+ statusCallbackUrl: callbackProperties.statusCallbackUrl,
207
+ encryptionKey: {
208
+ type: ["string", "null"],
209
+ description: "Optional key used to encrypt callback data.",
210
+ },
211
+ },
212
+ additionalProperties: false,
213
+ },
214
+ example: {
215
+ statusCallbackUrl: "https://example.com/webhooks/emotionlogic",
216
+ },
217
+ },
218
+ },
219
+ },
220
+ responses: {
221
+ 200: {$ref: "#/components/responses/DownloadAccepted"},
222
+ 404: {$ref: "#/components/responses/NotFound"},
223
+ ...standardErrorResponses,
224
+ },
225
+ },
226
+ },
227
+ "/apptone/v1/questionnaires": {
228
+ get: {
229
+ operationId: "apptoneListQuestionnaires",
230
+ tags: ["AppTone"],
231
+ summary: "List AppTone questionnaires",
232
+ description: "Returns private questionnaires available to the authenticated API user.",
233
+ "x-required-permission": "apptone_read",
234
+ parameters: [
235
+ {
236
+ name: "query",
237
+ in: "query",
238
+ schema: {type: "string"},
239
+ description: "Free-text search query.",
240
+ },
241
+ {
242
+ name: "tags",
243
+ in: "query",
244
+ schema: {
245
+ oneOf: [
246
+ {type: "string"},
247
+ {type: "array", items: {type: "string"}},
248
+ ],
249
+ },
250
+ style: "form",
251
+ explode: true,
252
+ },
253
+ {
254
+ name: "categories",
255
+ in: "query",
256
+ schema: {
257
+ oneOf: [
258
+ {type: "string"},
259
+ {type: "array", items: {type: "string"}},
260
+ ],
261
+ },
262
+ style: "form",
263
+ explode: true,
264
+ },
265
+ {
266
+ name: "languages",
267
+ in: "query",
268
+ schema: {
269
+ oneOf: [
270
+ {type: "string"},
271
+ {type: "array", items: {type: "string"}},
272
+ ],
273
+ },
274
+ style: "form",
275
+ explode: true,
276
+ },
277
+ ],
278
+ responses: {
279
+ 200: {
280
+ description: "Available questionnaires.",
281
+ content: {
282
+ "application/json": {
283
+ schema: {
284
+ type: "array",
285
+ items: {$ref: "#/components/schemas/Questionnaire"},
286
+ },
287
+ },
288
+ },
289
+ },
290
+ ...standardErrorResponses,
291
+ },
292
+ },
293
+ },
294
+ "/apptone/v1/questionnaires/{questionnaireId}/sendToCustomer": {
295
+ post: {
296
+ operationId: "apptoneSendToCustomer",
297
+ tags: ["AppTone"],
298
+ summary: "Send a questionnaire to a customer",
299
+ description: "Starts an asynchronous questionnaire report and returns its report ID.",
300
+ "x-required-permission": "apptone_read",
301
+ "x-callback-behavior": "The final report is delivered to statusCallbackUrl or statusCallbackEmail.",
302
+ parameters: [questionnaireIdParameter],
303
+ requestBody: {
304
+ required: true,
305
+ content: {
306
+ "application/json": {
307
+ schema: {
308
+ type: "object",
309
+ required: ["name", "phoneNumber"],
310
+ anyOf: [
311
+ {required: ["statusCallbackUrl"]},
312
+ {required: ["statusCallbackEmail"]},
313
+ ],
314
+ properties: {
315
+ name: {type: "string", minLength: 1},
316
+ phoneNumber: {type: "string", minLength: 1},
317
+ identifier: {type: "string"},
318
+ ...callbackProperties,
319
+ encryptionKey: {
320
+ type: ["string", "null"],
321
+ description: "Optional key used to encrypt callback data.",
322
+ },
323
+ addQuestionsText: {type: "boolean"},
324
+ },
325
+ additionalProperties: false,
326
+ },
327
+ example: {
328
+ name: "Example Customer",
329
+ phoneNumber: "+15551234567",
330
+ identifier: "customer-123",
331
+ statusCallbackUrl: "https://example.com/webhooks/emotionlogic",
332
+ addQuestionsText: true,
333
+ },
334
+ },
335
+ },
336
+ },
337
+ responses: {
338
+ 200: {$ref: "#/components/responses/ReportCreated"},
339
+ 404: {$ref: "#/components/responses/NotFound"},
340
+ ...standardErrorResponses,
341
+ },
342
+ },
343
+ },
344
+ "/feelgpt/v1/advisors": {
345
+ get: {
346
+ operationId: "feelgptListAdvisors",
347
+ tags: ["FeelGPT"],
348
+ summary: "List FeelGPT advisors",
349
+ description: "Returns advisors available to the authenticated API user.",
350
+ "x-required-permission": "feelgpt_app_usage",
351
+ parameters: [
352
+ {
353
+ name: "lang",
354
+ in: "query",
355
+ schema: {type: "string"},
356
+ description: "Preferred language.",
357
+ },
358
+ {
359
+ name: "feelGPTAdvisorId",
360
+ in: "query",
361
+ schema: {type: "string"},
362
+ description: "Optional advisor ID filter.",
363
+ },
364
+ ],
365
+ responses: {
366
+ 200: {
367
+ description: "Available FeelGPT advisors.",
368
+ content: {
369
+ "application/json": {
370
+ schema: {
371
+ type: "array",
372
+ items: {$ref: "#/components/schemas/Advisor"},
373
+ },
374
+ },
375
+ },
376
+ },
377
+ ...standardErrorResponses,
378
+ },
379
+ },
380
+ },
381
+ "/feelgpt/v1/advisors/{appId}/analyze": {
382
+ post: {
383
+ operationId: "feelgptAnalyze",
384
+ tags: ["FeelGPT"],
385
+ summary: "Analyze a media file with FeelGPT",
386
+ description: "Uploads media for asynchronous analysis and immediately returns a report ID.",
387
+ "x-required-permission": "feelgpt_app_usage",
388
+ "x-callback-behavior": "The final analysis is delivered to statusCallbackUrl or statusCallbackEmail.",
389
+ parameters: [appIdParameter],
390
+ requestBody: {
391
+ required: true,
392
+ content: {
393
+ "multipart/form-data": {
394
+ schema: {
395
+ type: "object",
396
+ required: ["file", "audioLanguage", "analysisLanguage"],
397
+ anyOf: [
398
+ {required: ["statusCallbackUrl"]},
399
+ {required: ["statusCallbackEmail"]},
400
+ ],
401
+ properties: {
402
+ file: {
403
+ type: "string",
404
+ format: "binary",
405
+ description: "Audio or video file to analyze.",
406
+ },
407
+ audioLanguage: {
408
+ type: "string",
409
+ description: "Language spoken in the uploaded media.",
410
+ },
411
+ analysisLanguage: {
412
+ type: "string",
413
+ description: "Language for the generated analysis.",
414
+ },
415
+ sttProvider: {
416
+ type: "string",
417
+ description: "Optional speech-to-text provider.",
418
+ },
419
+ sttModel: {
420
+ type: "string",
421
+ description: "Optional speech-to-text model.",
422
+ },
423
+ ...callbackProperties,
424
+ encryptionKey: {
425
+ type: ["string", "null"],
426
+ description: "Optional key used to encrypt callback data.",
427
+ },
428
+ sendPdf: {type: "boolean", default: false},
429
+ customerRequestId: {
430
+ type: "string",
431
+ maxLength: 36,
432
+ description: "Caller-defined correlation identifier.",
433
+ },
434
+ },
435
+ additionalProperties: false,
436
+ },
437
+ },
438
+ },
439
+ },
440
+ responses: {
441
+ 200: {$ref: "#/components/responses/ReportCreated"},
442
+ 404: {$ref: "#/components/responses/NotFound"},
443
+ ...standardErrorResponses,
444
+ },
445
+ "x-codeSamples": [
446
+ {
447
+ lang: "curl",
448
+ source: [
449
+ `curl -X POST -u "$EMOTIONLOGIC_API_USERNAME:$EMOTIONLOGIC_API_PASSWORD" "${PRODUCTION_BASE_URL}/feelgpt/v1/advisors/APP_ID/analyze"`,
450
+ " -F \"file=@./recording.mp3\"",
451
+ " -F \"audioLanguage=en\"",
452
+ " -F \"analysisLanguage=en\"",
453
+ " -F \"statusCallbackUrl=https://example.com/webhooks/emotionlogic\"",
454
+ ].join(" \\\n"),
455
+ },
456
+ ],
457
+ },
458
+ },
459
+ },
460
+ components: {
461
+ securitySchemes: {
462
+ basicAuth: {
463
+ type: "http",
464
+ scheme: "basic",
465
+ description: "API username and password from EmotionLogic Hub (Analyze Now API Keys).",
466
+ },
467
+ },
468
+ schemas: {
469
+ Error: {
470
+ type: "object",
471
+ required: ["errorCode", "error"],
472
+ properties: {
473
+ errorCode: {type: "string", examples: ["unauthorized"]},
474
+ error: {type: "string", examples: ["Permission denied."]},
475
+ },
476
+ additionalProperties: true,
477
+ },
478
+ ReportId: {
479
+ type: "object",
480
+ required: ["reportId"],
481
+ properties: {
482
+ reportId: {
483
+ type: "string",
484
+ format: "uuid",
485
+ description: "Use this ID to correlate callbacks and subsequent report requests.",
486
+ },
487
+ },
488
+ },
489
+ Questionnaire: {
490
+ type: "object",
491
+ required: ["name", "apptoneQuestionnaireId"],
492
+ properties: {
493
+ apptoneQuestionnaireId: {type: "string"},
494
+ name: {type: "string"},
495
+ language: {type: ["string", "null"]},
496
+ description: {type: ["string", "null"]},
497
+ },
498
+ },
499
+ Advisor: {
500
+ type: "object",
501
+ required: ["feelGPTAdvisorId", "name", "price"],
502
+ properties: {
503
+ feelGPTAdvisorId: {type: "string"},
504
+ name: {type: "string"},
505
+ description: {type: ["string", "null"]},
506
+ price: {type: "number"},
507
+ },
508
+ },
509
+ },
510
+ responses: {
511
+ ReportCreated: {
512
+ description: "The asynchronous operation was accepted.",
513
+ content: {
514
+ "application/json": {
515
+ schema: {$ref: "#/components/schemas/ReportId"},
516
+ },
517
+ },
518
+ },
519
+ DownloadAccepted: {
520
+ description: "The report asset was generated and callback delivery was scheduled.",
521
+ content: {
522
+ "application/json": {
523
+ schema: {
524
+ allOf: [
525
+ {$ref: "#/components/schemas/ReportId"},
526
+ {
527
+ type: "object",
528
+ properties: {
529
+ data: {
530
+ type: "string",
531
+ contentEncoding: "base64",
532
+ description: "Generated asset data when included in the immediate response.",
533
+ },
534
+ },
535
+ },
536
+ ],
537
+ },
538
+ },
539
+ },
540
+ },
541
+ BadRequest: {
542
+ description: "Invalid request.",
543
+ content: {
544
+ "application/json": {
545
+ schema: {$ref: "#/components/schemas/Error"},
546
+ },
547
+ },
548
+ },
549
+ Unauthorized: {
550
+ description: "Missing or invalid Basic Auth credentials.",
551
+ content: {
552
+ "application/json": {
553
+ schema: {$ref: "#/components/schemas/Error"},
554
+ },
555
+ },
556
+ },
557
+ Forbidden: {
558
+ description: "The API user lacks the required permission.",
559
+ content: {
560
+ "application/json": {
561
+ schema: {$ref: "#/components/schemas/Error"},
562
+ },
563
+ },
564
+ },
565
+ NotFound: {
566
+ description: "The requested item was not found.",
567
+ content: {
568
+ "application/json": {
569
+ schema: {$ref: "#/components/schemas/Error"},
570
+ },
571
+ },
572
+ },
573
+ InternalError: {
574
+ description: "Internal service error.",
575
+ content: {
576
+ "application/json": {
577
+ schema: {$ref: "#/components/schemas/Error"},
578
+ },
579
+ },
580
+ },
581
+ },
582
+ },
583
+ };
584
+
585
+ function findOperation(operationId) {
586
+ for (const [path, pathItem] of Object.entries(OPENAPI_DOCUMENT.paths)) {
587
+ for (const [method, operation] of Object.entries(pathItem)) {
588
+ if (operation.operationId === operationId) {
589
+ return {method: method.toUpperCase(), path, operation};
590
+ }
591
+ }
592
+ }
593
+
594
+ return undefined;
595
+ }
596
+
597
+ module.exports = {
598
+ OPENAPI_DOCUMENT,
599
+ OPERATION_IDS,
600
+ PRODUCTION_BASE_URL,
601
+ findOperation,
602
+ };
@@ -0,0 +1,120 @@
1
+ const {z} = require("zod");
2
+ const {
3
+ OPENAPI_DOCUMENT,
4
+ OPERATION_IDS,
5
+ PRODUCTION_BASE_URL,
6
+ findOperation,
7
+ } = require("./docs/openapi");
8
+
9
+ const OPENAPI_RESOURCE_URI = "emotionlogic://api/openapi.json";
10
+
11
+ function buildReference(operationId) {
12
+ if (!operationId) {
13
+ return OPENAPI_DOCUMENT;
14
+ }
15
+
16
+ const match = findOperation(operationId);
17
+ if (!match) {
18
+ throw new Error(`Unknown operationId: ${operationId}`);
19
+ }
20
+
21
+ return {
22
+ openapi: OPENAPI_DOCUMENT.openapi,
23
+ info: OPENAPI_DOCUMENT.info,
24
+ baseUrl: PRODUCTION_BASE_URL,
25
+ authentication: OPENAPI_DOCUMENT.components.securitySchemes.basicAuth,
26
+ integrationGuidance: OPENAPI_DOCUMENT["x-integration-guidance"],
27
+ operationId,
28
+ ...match,
29
+ components: OPENAPI_DOCUMENT.components,
30
+ };
31
+ }
32
+
33
+ function registerDocumentation(server) {
34
+ server.registerResource(
35
+ "emotionlogic-api-openapi",
36
+ OPENAPI_RESOURCE_URI,
37
+ {
38
+ title: "EmotionLogic API OpenAPI specification",
39
+ description: "Complete public EmotionLogic API contract for generating client applications.",
40
+ mimeType: "application/json",
41
+ },
42
+ async (uri) => ({
43
+ contents: [
44
+ {
45
+ uri: uri.href,
46
+ mimeType: "application/json",
47
+ text: JSON.stringify(OPENAPI_DOCUMENT, null, 2),
48
+ },
49
+ ],
50
+ })
51
+ );
52
+
53
+ server.registerTool(
54
+ "emotionlogic_get_api_reference",
55
+ {
56
+ title: "Get EmotionLogic API reference",
57
+ description: "Get the complete EmotionLogic API contract or documentation for one operation. No API credentials are required.",
58
+ inputSchema: z.object({
59
+ operationId: z.enum(OPERATION_IDS)
60
+ .optional()
61
+ .describe("Optional operation to return. Omit it to receive the complete OpenAPI contract."),
62
+ }),
63
+ },
64
+ async ({operationId}) => ({
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: JSON.stringify(buildReference(operationId), null, 2),
69
+ },
70
+ ],
71
+ })
72
+ );
73
+
74
+ server.registerPrompt(
75
+ "emotionlogic_generate_integration",
76
+ {
77
+ title: "Generate an EmotionLogic API integration",
78
+ description: "Generate application code that integrates with the public EmotionLogic API.",
79
+ argsSchema: {
80
+ language: z.string().min(1).describe("Programming language, for example JavaScript, Python, or C#"),
81
+ framework: z.string().optional().describe("Optional framework or HTTP client library"),
82
+ useCase: z.string().min(1).describe("What the generated application should do"),
83
+ operationId: z.enum(OPERATION_IDS).optional().describe("Optional API operation to focus on"),
84
+ },
85
+ },
86
+ ({language, framework, useCase, operationId}) => {
87
+ const reference = buildReference(operationId);
88
+ const stack = framework ? `${language} with ${framework}` : language;
89
+
90
+ return {
91
+ messages: [
92
+ {
93
+ role: "user",
94
+ content: {
95
+ type: "text",
96
+ text: [
97
+ `Generate production-ready ${stack} code for this use case: ${useCase}`,
98
+ "",
99
+ `Use the EmotionLogic API at ${PRODUCTION_BASE_URL}.`,
100
+ "Use HTTP Basic Auth and read the username and password from EMOTIONLOGIC_API_USERNAME and EMOTIONLOGIC_API_PASSWORD environment variables.",
101
+ "Never hardcode credentials. Validate inputs, handle non-2xx responses, and return actionable errors.",
102
+ "For asynchronous operations, preserve the returned reportId and implement the documented callback flow.",
103
+ "Set application/json or multipart/form-data exactly as required by the selected operation.",
104
+ "",
105
+ "API reference:",
106
+ JSON.stringify(reference, null, 2),
107
+ ].join("\n"),
108
+ },
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ );
114
+ }
115
+
116
+ module.exports = {
117
+ OPENAPI_RESOURCE_URI,
118
+ buildReference,
119
+ registerDocumentation,
120
+ };
package/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ const {McpServer} = require("@modelcontextprotocol/sdk/server/mcp.js");
4
+ const {StdioServerTransport} = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const {registerAppToneTools} = require("./tools/apptone");
6
+ const {registerFeelGptTools} = require("./tools/feelgpt");
7
+ const {registerDocumentation} = require("./documentation");
8
+
9
+ function createServer() {
10
+ const server = new McpServer({
11
+ name: "emotionlogic",
12
+ version: "1.0.0",
13
+ });
14
+
15
+ registerAppToneTools(server);
16
+ registerFeelGptTools(server);
17
+ registerDocumentation(server);
18
+
19
+ return server;
20
+ }
21
+
22
+ async function main() {
23
+ const transport = new StdioServerTransport();
24
+ await createServer().connect(transport);
25
+ console.error("EmotionLogic MCP server is running on stdio.");
26
+ }
27
+
28
+ if (require.main === module) {
29
+ main().catch((error) => {
30
+ console.error("Failed to start EmotionLogic MCP server.", error);
31
+ process.exitCode = 1;
32
+ });
33
+ }
34
+
35
+ module.exports = {
36
+ createServer,
37
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@emotionlogic/mcp",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Official EmotionLogic MCP server for AppTone and FeelGPT",
6
+ "license": "UNLICENSED",
7
+ "author": {
8
+ "name": "EmotionLogic"
9
+ },
10
+ "homepage": "https://app.emotionlogic.ai/",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "index.js",
15
+ "bin": {
16
+ "emotionlogic-mcp": "index.js"
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "client.js",
21
+ "documentation.js",
22
+ "docs",
23
+ "tools",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "emotionlogic",
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "apptone",
31
+ "feelgpt"
32
+ ],
33
+ "scripts": {
34
+ "start": "node index.js",
35
+ "check": "node --check index.js && node --check client.js && node --check documentation.js && node --check docs/openapi.js && node --check tools/apptone.js && node --check tools/feelgpt.js && node --check test/smoke.test.js",
36
+ "test": "node --test test/smoke.test.js"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "1.29.0",
43
+ "axios": "^1.18.1",
44
+ "form-data": "^4.0.6",
45
+ "zod": "^4.4.3"
46
+ },
47
+ "overrides": {
48
+ "@hono/node-server": "2.0.10"
49
+ }
50
+ }
@@ -0,0 +1,127 @@
1
+ const {z} = require("zod");
2
+ const {requestJson, runTool} = require("../client");
3
+
4
+ const callbackFields = {
5
+ statusCallbackUrl: z.string().url().optional(),
6
+ statusCallbackEmail: z.string().email().optional(),
7
+ };
8
+
9
+ function encodePathSegment(value) {
10
+ return encodeURIComponent(value);
11
+ }
12
+
13
+ function requireCallback({statusCallbackUrl, statusCallbackEmail}) {
14
+ if (!statusCallbackUrl && !statusCallbackEmail) {
15
+ throw new Error("Either statusCallbackUrl or statusCallbackEmail must be provided.");
16
+ }
17
+ }
18
+
19
+ function registerAppToneTools(server) {
20
+ server.registerTool(
21
+ "apptone_cancel_report",
22
+ {
23
+ description: "Cancel an AppTone questionnaire report.",
24
+ inputSchema: z.object({
25
+ reportId: z.string().min(1).describe("AppTone report ID"),
26
+ }),
27
+ },
28
+ ({reportId}) => runTool(() => requestJson({
29
+ method: "post",
30
+ path: `/apptone/v1/reports/${encodePathSegment(reportId)}/cancel`,
31
+ }))
32
+ );
33
+
34
+ server.registerTool(
35
+ "apptone_download_pdf",
36
+ {
37
+ description: "Request AppTone report PDF delivery to a callback URL.",
38
+ inputSchema: z.object({
39
+ reportId: z.string().min(1).describe("AppTone report ID"),
40
+ statusCallbackUrl: z.string().url().describe("URL that receives the generated PDF"),
41
+ encryptionKey: z.string().nullable().optional(),
42
+ }),
43
+ },
44
+ ({reportId, ...body}) => runTool(() => requestJson({
45
+ method: "post",
46
+ path: `/apptone/v1/reports/${encodePathSegment(reportId)}/downloadPdf`,
47
+ body,
48
+ }))
49
+ );
50
+
51
+ server.registerTool(
52
+ "apptone_get_analysis_result",
53
+ {
54
+ description: "Get the analysis result for an AppTone report.",
55
+ inputSchema: z.object({
56
+ reportId: z.string().min(1).describe("AppTone report ID"),
57
+ }),
58
+ },
59
+ ({reportId}) => runTool(() => requestJson({
60
+ method: "get",
61
+ path: `/apptone/v1/reports/${encodePathSegment(reportId)}/getAnalysisResult`,
62
+ }))
63
+ );
64
+
65
+ server.registerTool(
66
+ "apptone_download_audio",
67
+ {
68
+ description: "Request AppTone report audio delivery to a callback URL.",
69
+ inputSchema: z.object({
70
+ reportId: z.string().min(1).describe("AppTone report ID"),
71
+ statusCallbackUrl: z.string().url().describe("URL that receives the report audio"),
72
+ encryptionKey: z.string().nullable().optional(),
73
+ }),
74
+ },
75
+ ({reportId, ...body}) => runTool(() => requestJson({
76
+ method: "post",
77
+ path: `/apptone/v1/reports/${encodePathSegment(reportId)}/downloadAudio`,
78
+ body,
79
+ }))
80
+ );
81
+
82
+ server.registerTool(
83
+ "apptone_list_questionnaires",
84
+ {
85
+ description: "List AppTone questionnaires available to the API user.",
86
+ inputSchema: z.object({
87
+ query: z.string().optional(),
88
+ tags: z.union([z.string(), z.array(z.string())]).optional(),
89
+ categories: z.union([z.string(), z.array(z.string())]).optional(),
90
+ languages: z.union([z.string(), z.array(z.string())]).optional(),
91
+ }),
92
+ },
93
+ (query) => runTool(() => requestJson({
94
+ method: "get",
95
+ path: "/apptone/v1/questionnaires",
96
+ query,
97
+ }))
98
+ );
99
+
100
+ server.registerTool(
101
+ "apptone_send_to_customer",
102
+ {
103
+ description: "Send an AppTone questionnaire to a customer and return its report ID.",
104
+ inputSchema: z.object({
105
+ questionnaireId: z.string().min(1).describe("AppTone questionnaire ID"),
106
+ name: z.string().min(1).describe("Customer name"),
107
+ phoneNumber: z.string().min(1).describe("Customer phone number"),
108
+ identifier: z.string().optional(),
109
+ ...callbackFields,
110
+ encryptionKey: z.string().nullable().optional(),
111
+ addQuestionsText: z.boolean().optional(),
112
+ }),
113
+ },
114
+ ({questionnaireId, ...body}) => runTool(async () => {
115
+ requireCallback(body);
116
+ return requestJson({
117
+ method: "post",
118
+ path: `/apptone/v1/questionnaires/${encodePathSegment(questionnaireId)}/sendToCustomer`,
119
+ body,
120
+ });
121
+ })
122
+ );
123
+ }
124
+
125
+ module.exports = {
126
+ registerAppToneTools,
127
+ };
@@ -0,0 +1,63 @@
1
+ const {z} = require("zod");
2
+ const {requestJson, requestMultipart, runTool} = require("../client");
3
+
4
+ function encodePathSegment(value) {
5
+ return encodeURIComponent(value);
6
+ }
7
+
8
+ function requireCallback({statusCallbackUrl, statusCallbackEmail}) {
9
+ if (!statusCallbackUrl && !statusCallbackEmail) {
10
+ throw new Error("Either statusCallbackUrl or statusCallbackEmail must be provided.");
11
+ }
12
+ }
13
+
14
+ function registerFeelGptTools(server) {
15
+ server.registerTool(
16
+ "feelgpt_list_advisors",
17
+ {
18
+ description: "List FeelGPT advisors available to the API user.",
19
+ inputSchema: z.object({
20
+ lang: z.string().optional(),
21
+ feelGPTAdvisorId: z.string().optional(),
22
+ }),
23
+ },
24
+ (query) => runTool(() => requestJson({
25
+ method: "get",
26
+ path: "/feelgpt/v1/advisors",
27
+ query,
28
+ }))
29
+ );
30
+
31
+ server.registerTool(
32
+ "feelgpt_analyze",
33
+ {
34
+ description: "Upload a local media file for asynchronous FeelGPT analysis.",
35
+ inputSchema: z.object({
36
+ appId: z.string().min(1).describe("FeelGPT advisor application ID"),
37
+ filePath: z.string().min(1).describe("Local path to the media file"),
38
+ audioLanguage: z.string().min(1),
39
+ analysisLanguage: z.string().min(1),
40
+ sttProvider: z.string().optional(),
41
+ sttModel: z.string().optional(),
42
+ statusCallbackUrl: z.string().url().optional(),
43
+ statusCallbackEmail: z.string().email().optional(),
44
+ encryptionKey: z.string().nullable().optional(),
45
+ sendPdf: z.boolean().optional(),
46
+ customerRequestId: z.string().max(36).optional(),
47
+ }),
48
+ },
49
+ ({appId, filePath, ...fields}) => runTool(async () => {
50
+ requireCallback(fields);
51
+ return requestMultipart({
52
+ path: `/feelgpt/v1/advisors/${encodePathSegment(appId)}/analyze`,
53
+ fields,
54
+ filePath,
55
+ fileField: "file",
56
+ });
57
+ })
58
+ );
59
+ }
60
+
61
+ module.exports = {
62
+ registerFeelGptTools,
63
+ };