@linkedapi/mcp 0.3.11

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 (212) hide show
  1. package/.dockerignore +11 -0
  2. package/.github/workflows/merge-main.yaml +136 -0
  3. package/.github/workflows/pr-main.yaml +42 -0
  4. package/.husky/pre-commit +5 -0
  5. package/.prettierrc.js +21 -0
  6. package/.vscode/launch.json +19 -0
  7. package/Dockerfile +39 -0
  8. package/LICENSE +21 -0
  9. package/README.md +17 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +189 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/linked-api-server.d.ts +13 -0
  15. package/dist/linked-api-server.d.ts.map +1 -0
  16. package/dist/linked-api-server.js +127 -0
  17. package/dist/linked-api-server.js.map +1 -0
  18. package/dist/linked-api-tools.d.ts +8 -0
  19. package/dist/linked-api-tools.d.ts.map +1 -0
  20. package/dist/linked-api-tools.js +72 -0
  21. package/dist/linked-api-tools.js.map +1 -0
  22. package/dist/prompts/index.d.ts +10 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +190 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/tools/check-connection-status.d.ts +13 -0
  27. package/dist/tools/check-connection-status.d.ts.map +1 -0
  28. package/dist/tools/check-connection-status.js +31 -0
  29. package/dist/tools/check-connection-status.js.map +1 -0
  30. package/dist/tools/comment-on-post.d.ts +15 -0
  31. package/dist/tools/comment-on-post.d.ts.map +1 -0
  32. package/dist/tools/comment-on-post.js +41 -0
  33. package/dist/tools/comment-on-post.js.map +1 -0
  34. package/dist/tools/create-post.d.ts +23 -0
  35. package/dist/tools/create-post.d.ts.map +1 -0
  36. package/dist/tools/create-post.js +67 -0
  37. package/dist/tools/create-post.js.map +1 -0
  38. package/dist/tools/execute-custom-workflow.d.ts +13 -0
  39. package/dist/tools/execute-custom-workflow.d.ts.map +1 -0
  40. package/dist/tools/execute-custom-workflow.js +26 -0
  41. package/dist/tools/execute-custom-workflow.js.map +1 -0
  42. package/dist/tools/fetch-company.d.ts +34 -0
  43. package/dist/tools/fetch-company.d.ts.map +1 -0
  44. package/dist/tools/fetch-company.js +139 -0
  45. package/dist/tools/fetch-company.js.map +1 -0
  46. package/dist/tools/fetch-person.d.ts +32 -0
  47. package/dist/tools/fetch-person.d.ts.map +1 -0
  48. package/dist/tools/fetch-person.js +129 -0
  49. package/dist/tools/fetch-person.js.map +1 -0
  50. package/dist/tools/fetch-post.d.ts +26 -0
  51. package/dist/tools/fetch-post.d.ts.map +1 -0
  52. package/dist/tools/fetch-post.js +82 -0
  53. package/dist/tools/fetch-post.js.map +1 -0
  54. package/dist/tools/get-api-usage-stats.d.ts +19 -0
  55. package/dist/tools/get-api-usage-stats.d.ts.map +1 -0
  56. package/dist/tools/get-api-usage-stats.js +40 -0
  57. package/dist/tools/get-api-usage-stats.js.map +1 -0
  58. package/dist/tools/get-conversation.d.ts +26 -0
  59. package/dist/tools/get-conversation.d.ts.map +1 -0
  60. package/dist/tools/get-conversation.js +59 -0
  61. package/dist/tools/get-conversation.js.map +1 -0
  62. package/dist/tools/get-workflow-result.d.ts +50 -0
  63. package/dist/tools/get-workflow-result.d.ts.map +1 -0
  64. package/dist/tools/get-workflow-result.js +46 -0
  65. package/dist/tools/get-workflow-result.js.map +1 -0
  66. package/dist/tools/nv-fetch-company.d.ts +30 -0
  67. package/dist/tools/nv-fetch-company.d.ts.map +1 -0
  68. package/dist/tools/nv-fetch-company.js +124 -0
  69. package/dist/tools/nv-fetch-company.js.map +1 -0
  70. package/dist/tools/nv-fetch-person.d.ts +13 -0
  71. package/dist/tools/nv-fetch-person.d.ts.map +1 -0
  72. package/dist/tools/nv-fetch-person.js +31 -0
  73. package/dist/tools/nv-fetch-person.js.map +1 -0
  74. package/dist/tools/nv-get-conversation.d.ts +26 -0
  75. package/dist/tools/nv-get-conversation.d.ts.map +1 -0
  76. package/dist/tools/nv-get-conversation.js +59 -0
  77. package/dist/tools/nv-get-conversation.js.map +1 -0
  78. package/dist/tools/nv-search-companies.d.ts +32 -0
  79. package/dist/tools/nv-search-companies.d.ts.map +1 -0
  80. package/dist/tools/nv-search-companies.js +106 -0
  81. package/dist/tools/nv-search-companies.js.map +1 -0
  82. package/dist/tools/nv-search-people.d.ts +25 -0
  83. package/dist/tools/nv-search-people.d.ts.map +1 -0
  84. package/dist/tools/nv-search-people.js +99 -0
  85. package/dist/tools/nv-search-people.js.map +1 -0
  86. package/dist/tools/nv-send-message.d.ts +15 -0
  87. package/dist/tools/nv-send-message.d.ts.map +1 -0
  88. package/dist/tools/nv-send-message.js +41 -0
  89. package/dist/tools/nv-send-message.js.map +1 -0
  90. package/dist/tools/nv-sync-conversation.d.ts +13 -0
  91. package/dist/tools/nv-sync-conversation.d.ts.map +1 -0
  92. package/dist/tools/nv-sync-conversation.js +31 -0
  93. package/dist/tools/nv-sync-conversation.js.map +1 -0
  94. package/dist/tools/react-to-post.d.ts +22 -0
  95. package/dist/tools/react-to-post.d.ts.map +1 -0
  96. package/dist/tools/react-to-post.js +42 -0
  97. package/dist/tools/react-to-post.js.map +1 -0
  98. package/dist/tools/remove-connection.d.ts +13 -0
  99. package/dist/tools/remove-connection.d.ts.map +1 -0
  100. package/dist/tools/remove-connection.js +31 -0
  101. package/dist/tools/remove-connection.js.map +1 -0
  102. package/dist/tools/retrieve-connections.d.ts +24 -0
  103. package/dist/tools/retrieve-connections.d.ts.map +1 -0
  104. package/dist/tools/retrieve-connections.js +90 -0
  105. package/dist/tools/retrieve-connections.js.map +1 -0
  106. package/dist/tools/retrieve-pending-requests.d.ts +10 -0
  107. package/dist/tools/retrieve-pending-requests.d.ts.map +1 -0
  108. package/dist/tools/retrieve-pending-requests.js +23 -0
  109. package/dist/tools/retrieve-pending-requests.js.map +1 -0
  110. package/dist/tools/retrieve-performance.d.ts +10 -0
  111. package/dist/tools/retrieve-performance.d.ts.map +1 -0
  112. package/dist/tools/retrieve-performance.js +23 -0
  113. package/dist/tools/retrieve-performance.js.map +1 -0
  114. package/dist/tools/retrieve-ssi.d.ts +10 -0
  115. package/dist/tools/retrieve-ssi.d.ts.map +1 -0
  116. package/dist/tools/retrieve-ssi.js +23 -0
  117. package/dist/tools/retrieve-ssi.js.map +1 -0
  118. package/dist/tools/search-companies.d.ts +28 -0
  119. package/dist/tools/search-companies.d.ts.map +1 -0
  120. package/dist/tools/search-companies.js +86 -0
  121. package/dist/tools/search-companies.js.map +1 -0
  122. package/dist/tools/search-people.d.ts +24 -0
  123. package/dist/tools/search-people.d.ts.map +1 -0
  124. package/dist/tools/search-people.js +90 -0
  125. package/dist/tools/search-people.js.map +1 -0
  126. package/dist/tools/send-connection-request.d.ts +15 -0
  127. package/dist/tools/send-connection-request.d.ts.map +1 -0
  128. package/dist/tools/send-connection-request.js +41 -0
  129. package/dist/tools/send-connection-request.js.map +1 -0
  130. package/dist/tools/send-message.d.ts +14 -0
  131. package/dist/tools/send-message.d.ts.map +1 -0
  132. package/dist/tools/send-message.js +36 -0
  133. package/dist/tools/send-message.js.map +1 -0
  134. package/dist/tools/sync-conversation.d.ts +13 -0
  135. package/dist/tools/sync-conversation.d.ts.map +1 -0
  136. package/dist/tools/sync-conversation.js +31 -0
  137. package/dist/tools/sync-conversation.js.map +1 -0
  138. package/dist/tools/withdraw-connection-request.d.ts +14 -0
  139. package/dist/tools/withdraw-connection-request.d.ts.map +1 -0
  140. package/dist/tools/withdraw-connection-request.js +36 -0
  141. package/dist/tools/withdraw-connection-request.js.map +1 -0
  142. package/dist/utils/define-request-timeout.d.ts +2 -0
  143. package/dist/utils/define-request-timeout.d.ts.map +1 -0
  144. package/dist/utils/define-request-timeout.js +20 -0
  145. package/dist/utils/define-request-timeout.js.map +1 -0
  146. package/dist/utils/execute-with-progress.d.ts +8 -0
  147. package/dist/utils/execute-with-progress.d.ts.map +1 -0
  148. package/dist/utils/execute-with-progress.js +75 -0
  149. package/dist/utils/execute-with-progress.js.map +1 -0
  150. package/dist/utils/handle-linked-api-error.d.ts +3 -0
  151. package/dist/utils/handle-linked-api-error.d.ts.map +1 -0
  152. package/dist/utils/handle-linked-api-error.js +29 -0
  153. package/dist/utils/handle-linked-api-error.js.map +1 -0
  154. package/dist/utils/json-http-transport.d.ts +26 -0
  155. package/dist/utils/json-http-transport.d.ts.map +1 -0
  156. package/dist/utils/json-http-transport.js +265 -0
  157. package/dist/utils/json-http-transport.js.map +1 -0
  158. package/dist/utils/linked-api-tool.d.ts +28 -0
  159. package/dist/utils/linked-api-tool.d.ts.map +1 -0
  160. package/dist/utils/linked-api-tool.js +25 -0
  161. package/dist/utils/linked-api-tool.js.map +1 -0
  162. package/dist/utils/logger.d.ts +8 -0
  163. package/dist/utils/logger.d.ts.map +1 -0
  164. package/dist/utils/logger.js +81 -0
  165. package/dist/utils/logger.js.map +1 -0
  166. package/dist/utils/types.d.ts +16 -0
  167. package/dist/utils/types.d.ts.map +1 -0
  168. package/dist/utils/types.js +3 -0
  169. package/dist/utils/types.js.map +1 -0
  170. package/eslint.config.js +118 -0
  171. package/package.json +62 -0
  172. package/src/index.ts +212 -0
  173. package/src/linked-api-server.ts +159 -0
  174. package/src/linked-api-tools.ts +72 -0
  175. package/src/prompts/index.ts +190 -0
  176. package/src/tools/check-connection-status.ts +39 -0
  177. package/src/tools/comment-on-post.ts +42 -0
  178. package/src/tools/create-post.ts +71 -0
  179. package/src/tools/execute-custom-workflow.ts +28 -0
  180. package/src/tools/fetch-company.ts +154 -0
  181. package/src/tools/fetch-person.ts +145 -0
  182. package/src/tools/fetch-post.ts +90 -0
  183. package/src/tools/get-api-usage-stats.ts +48 -0
  184. package/src/tools/get-conversation.ts +76 -0
  185. package/src/tools/get-workflow-result.ts +62 -0
  186. package/src/tools/nv-fetch-company.ts +136 -0
  187. package/src/tools/nv-fetch-person.ts +31 -0
  188. package/src/tools/nv-get-conversation.ts +76 -0
  189. package/src/tools/nv-search-companies.ts +114 -0
  190. package/src/tools/nv-search-people.ts +107 -0
  191. package/src/tools/nv-send-message.ts +42 -0
  192. package/src/tools/nv-sync-conversation.ts +32 -0
  193. package/src/tools/react-to-post.ts +44 -0
  194. package/src/tools/remove-connection.ts +32 -0
  195. package/src/tools/retrieve-connections.ts +105 -0
  196. package/src/tools/retrieve-pending-requests.ts +23 -0
  197. package/src/tools/retrieve-performance.ts +23 -0
  198. package/src/tools/retrieve-ssi.ts +23 -0
  199. package/src/tools/search-companies.ts +93 -0
  200. package/src/tools/search-people.ts +97 -0
  201. package/src/tools/send-connection-request.ts +46 -0
  202. package/src/tools/send-message.ts +36 -0
  203. package/src/tools/sync-conversation.ts +32 -0
  204. package/src/tools/withdraw-connection-request.ts +41 -0
  205. package/src/utils/define-request-timeout.ts +17 -0
  206. package/src/utils/execute-with-progress.ts +93 -0
  207. package/src/utils/handle-linked-api-error.ts +27 -0
  208. package/src/utils/json-http-transport.ts +325 -0
  209. package/src/utils/linked-api-tool.ts +58 -0
  210. package/src/utils/logger.ts +84 -0
  211. package/src/utils/types.ts +17 -0
  212. package/tsconfig.json +36 -0
@@ -0,0 +1,93 @@
1
+ import { LinkedApiWorkflowTimeoutError, Operation, TMappedResponse } from '@linkedapi/node';
2
+
3
+ import { LinkedApiProgressNotification } from './types';
4
+
5
+ export async function executeWithProgress<TParams, TResult>(
6
+ progressCallback: (progress: LinkedApiProgressNotification) => void,
7
+ operation: Operation<TParams, TResult>,
8
+ workflowTimeout: number,
9
+ {
10
+ params,
11
+ workflowId,
12
+ progressToken,
13
+ }: { params?: TParams; workflowId?: string; progressToken?: string | number } = {},
14
+ ): Promise<TMappedResponse<TResult>> {
15
+ let progress = 0;
16
+
17
+ progressCallback({
18
+ progressToken,
19
+ progress,
20
+ total: 100,
21
+ message: `Starting workflow ${operation.operationName}...`,
22
+ });
23
+
24
+ const interval = setInterval(
25
+ () => {
26
+ if (progress < 50) {
27
+ progress += 5;
28
+ } else if (progress < 98) {
29
+ progress += 1;
30
+ }
31
+
32
+ progressCallback({
33
+ progressToken,
34
+ progress,
35
+ total: 100,
36
+ message: `Executing workflow ${operation.operationName}...`,
37
+ });
38
+ },
39
+ Math.max(workflowTimeout / 20, 10000),
40
+ );
41
+
42
+ try {
43
+ if (!workflowId) {
44
+ workflowId = await operation.execute(params as TParams);
45
+ }
46
+ const result = await operation.result(workflowId, {
47
+ timeout: workflowTimeout,
48
+ });
49
+ clearInterval(interval);
50
+
51
+ progressCallback({
52
+ progressToken,
53
+ progress: 100,
54
+ total: 100,
55
+ message: `Workflow ${operation.operationName} completed successfully`,
56
+ });
57
+
58
+ return result;
59
+ } catch (error) {
60
+ clearInterval(interval);
61
+ if (error instanceof LinkedApiWorkflowTimeoutError) {
62
+ throw generateTimeoutError(error);
63
+ }
64
+
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ function generateTimeoutError(error: LinkedApiWorkflowTimeoutError) {
70
+ const restoreMessage = `Workflow is running in the background. Continue listening for updates.
71
+
72
+ ACTION REQUIRED (MCP CLIENT):
73
+ - Do not retry the original tool; the workflow is already running
74
+ - Call the get_workflow_result tool with the parameters below and keep listening until completion
75
+
76
+ PARAMETERS TO USE:
77
+ - workflowId: ${error.workflowId}
78
+ - operationName: ${error.operationName}
79
+
80
+ GENERIC MCP CALL EXAMPLE:
81
+ {
82
+ "name": "get_workflow_result",
83
+ "arguments": {
84
+ "workflowId": "${error.workflowId}",
85
+ "operationName": "${error.operationName}"
86
+ }
87
+ }
88
+
89
+ Background processing keeps the MCP client responsive while the workflow completes.`;
90
+
91
+ error.message = restoreMessage;
92
+ return error;
93
+ }
@@ -0,0 +1,27 @@
1
+ import { LinkedApiError, LinkedApiWorkflowTimeoutError } from '@linkedapi/node';
2
+
3
+ import { authenticationPrompt } from '../prompts';
4
+
5
+ export function handleLinkedApiError(error: LinkedApiError): object {
6
+ if (error instanceof LinkedApiWorkflowTimeoutError) {
7
+ const { message, workflowId, operationName } = error;
8
+ return {
9
+ message,
10
+ workflowId,
11
+ operationName,
12
+ };
13
+ }
14
+ switch (error.type) {
15
+ case 'identificationTokenRequired':
16
+ case 'linkedApiTokenRequired':
17
+ case 'invalidLinkedApiToken':
18
+ case 'invalidIdentificationToken':
19
+ return {
20
+ message: authenticationPrompt,
21
+ };
22
+ }
23
+ return {
24
+ message: error.message,
25
+ type: error.type,
26
+ };
27
+ }
@@ -0,0 +1,325 @@
1
+ import { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
2
+ import {
3
+ isJSONRPCError,
4
+ isJSONRPCRequest,
5
+ isJSONRPCResponse,
6
+ JSONRPCMessage,
7
+ JSONRPCMessageSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { IncomingMessage, ServerResponse } from 'node:http';
10
+
11
+ import { logger } from './logger';
12
+
13
+ type RequestId = number | string;
14
+
15
+ type ConnectionContext = {
16
+ res: ServerResponse;
17
+ orderedIds: RequestId[];
18
+ pendingIds: Set<RequestId>;
19
+ responses: Map<RequestId, JSONRPCMessage>;
20
+ };
21
+
22
+ type SseContext = {
23
+ res: ServerResponse;
24
+ keepalive: NodeJS.Timeout;
25
+ };
26
+
27
+ export class JsonHTTPServerTransport implements Transport {
28
+ public onclose?: () => void;
29
+ public onerror?: (error: Error) => void;
30
+ public onmessage?: (
31
+ message: JSONRPCMessage,
32
+ extra?: {
33
+ requestInfo?: {
34
+ headers: IncomingMessage['headers'];
35
+ method?: string;
36
+ transport?: 'http' | 'sse';
37
+ };
38
+ authInfo?: unknown;
39
+ },
40
+ ) => void;
41
+
42
+ private started = false;
43
+ private requestIdToConn = new Map<RequestId, string>();
44
+ private connections = new Map<string, ConnectionContext>();
45
+ private sse?: SseContext;
46
+
47
+ async start(): Promise<void> {
48
+ if (this.started) throw new Error('Transport already started');
49
+ this.started = true;
50
+ }
51
+
52
+ async close(): Promise<void> {
53
+ if (this.sse) {
54
+ try {
55
+ clearInterval(this.sse.keepalive);
56
+ if (!this.sse.res.writableEnded) {
57
+ this.sse.res.end();
58
+ }
59
+ } catch {
60
+ // ignore
61
+ }
62
+ logger.info('SSE connection terminated during transport close');
63
+ this.sse = undefined;
64
+ }
65
+ this.connections.forEach((ctx) => {
66
+ try {
67
+ if (!ctx.res.writableEnded) {
68
+ ctx.res.end();
69
+ }
70
+ } catch {
71
+ // ignore
72
+ }
73
+ });
74
+ this.connections.clear();
75
+ this.requestIdToConn.clear();
76
+ this.onclose?.();
77
+ }
78
+
79
+ async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
80
+ let relatedId = options?.relatedRequestId;
81
+ if (isJSONRPCResponse(message) || isJSONRPCError(message)) {
82
+ relatedId = message.id;
83
+ }
84
+ // If a related HTTP connection is pending, complete it with JSON
85
+ if (relatedId !== undefined) {
86
+ const connId = this.requestIdToConn.get(relatedId);
87
+ if (connId) {
88
+ const ctx = this.connections.get(connId);
89
+ if (!ctx) throw new Error(`HTTP connection closed for request ${String(relatedId)}`);
90
+
91
+ ctx.responses.set(relatedId, message);
92
+ const allReady = ctx.orderedIds.every((id) => ctx.responses.has(id));
93
+ if (!allReady) return;
94
+
95
+ const body =
96
+ ctx.orderedIds.length === 1
97
+ ? ctx.responses.get(ctx.orderedIds[0]!)
98
+ : ctx.orderedIds.map((id) => ctx.responses.get(id));
99
+
100
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
101
+ ctx.res.writeHead(200, headers);
102
+ ctx.res.end(JSON.stringify(body));
103
+
104
+ this.connections.delete(connId);
105
+ ctx.orderedIds.forEach((id) => this.requestIdToConn.delete(id));
106
+ return;
107
+ }
108
+ }
109
+
110
+ // Otherwise, if SSE is connected, stream server -> client messages via SSE
111
+ if (this.sse && !this.sse.res.writableEnded) {
112
+ const line = `data: ${JSON.stringify(message)}\n\n`;
113
+ this.sse.res.write(line);
114
+ return;
115
+ }
116
+
117
+ // No pending HTTP response and no SSE: drop notifications without a target
118
+ return;
119
+ }
120
+
121
+ // Handle HTTP requests: supports POST for JSON and GET for SSE
122
+ async handleRequest(
123
+ req: IncomingMessage & { auth?: unknown },
124
+ res: ServerResponse,
125
+ parsedBody?: unknown,
126
+ ): Promise<void> {
127
+ try {
128
+ // SSE endpoint: accept GET with text/event-stream
129
+ const acceptHeader = (req.headers['accept'] || '').toString();
130
+ if (req.method === 'GET' && acceptHeader.includes('text/event-stream')) {
131
+ // Close previous SSE if any
132
+ if (this.sse) {
133
+ try {
134
+ clearInterval(this.sse.keepalive);
135
+ if (!this.sse.res.writableEnded) this.sse.res.end();
136
+ } catch {
137
+ // ignore
138
+ }
139
+ }
140
+
141
+ res.writeHead(200, {
142
+ 'Content-Type': 'text/event-stream',
143
+ 'Cache-Control': 'no-cache, no-transform',
144
+ Connection: 'keep-alive',
145
+ 'X-Accel-Buffering': 'no',
146
+ });
147
+ // Send an initial comment to establish the stream
148
+ res.write(': connected\n\n');
149
+
150
+ const keepalive = setInterval(() => {
151
+ if (res.writableEnded) return;
152
+ res.write('event: ping\ndata: {}\n\n');
153
+ }, 25000);
154
+
155
+ this.sse = {
156
+ res,
157
+ keepalive,
158
+ };
159
+ logger.info({ headers: req.headers }, 'SSE connection established');
160
+
161
+ res.on('close', () => {
162
+ try {
163
+ clearInterval(keepalive);
164
+ } finally {
165
+ logger.info('SSE connection closed by client');
166
+ this.sse = undefined;
167
+ }
168
+ });
169
+ return;
170
+ }
171
+
172
+ if (req.method !== 'POST') {
173
+ res.writeHead(405, { Allow: 'POST' }).end(
174
+ JSON.stringify({
175
+ jsonrpc: '2.0',
176
+ error: {
177
+ code: -32000,
178
+ message: 'Method not allowed. Only POST is supported.',
179
+ },
180
+ id: null,
181
+ }),
182
+ );
183
+ logger.warn(
184
+ {
185
+ method: req.method,
186
+ headers: req.headers,
187
+ },
188
+ 'Rejected non-POST HTTP request',
189
+ );
190
+ return;
191
+ }
192
+
193
+ // For POST, allow generic Accepts; when SSE is connected, we don't require JSON accept
194
+ const accept = req.headers['accept'];
195
+ const acceptsJson = !!(accept && accept.includes('application/json'));
196
+ const sseActive = !!this.sse && !this.sse.res.writableEnded;
197
+ if (!acceptsJson && !sseActive) {
198
+ res.writeHead(406);
199
+ res.end(
200
+ JSON.stringify({
201
+ jsonrpc: '2.0',
202
+ error: {
203
+ code: -32000,
204
+ message: 'Not Acceptable: Client must accept application/json or have SSE open',
205
+ },
206
+ id: null,
207
+ }),
208
+ );
209
+ logger.warn({ headers: req.headers }, 'Rejected POST due to unacceptable Accept header');
210
+ return;
211
+ }
212
+
213
+ const ct = req.headers['content-type'];
214
+ if (!(ct && ct.includes('application/json'))) {
215
+ res.writeHead(415);
216
+ res.end(
217
+ JSON.stringify({
218
+ jsonrpc: '2.0',
219
+ error: {
220
+ code: -32000,
221
+ message: 'Unsupported Media Type: Content-Type must be application/json',
222
+ },
223
+ id: null,
224
+ }),
225
+ );
226
+ logger.warn({ headers: req.headers }, 'Rejected POST due to unsupported Content-Type');
227
+ return;
228
+ }
229
+
230
+ let raw: unknown = parsedBody;
231
+ if (raw === undefined) {
232
+ const chunks: Buffer[] = [];
233
+ await new Promise<void>((resolve, reject) => {
234
+ req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
235
+ req.on('end', () => resolve());
236
+ req.on('error', reject);
237
+ });
238
+ raw = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
239
+ }
240
+
241
+ const messages = Array.isArray(raw)
242
+ ? (raw as unknown[]).map((m) => JSONRPCMessageSchema.parse(m))
243
+ : [JSONRPCMessageSchema.parse(raw)];
244
+
245
+ const hasRequests = messages.some(isJSONRPCRequest);
246
+ if (!hasRequests) {
247
+ res.writeHead(202).end();
248
+ for (const msg of messages) {
249
+ this.onmessage?.(msg, {
250
+ requestInfo: {
251
+ headers: req.headers,
252
+ method: req.method,
253
+ transport: 'http',
254
+ },
255
+ authInfo: req.auth,
256
+ });
257
+ }
258
+ logger.info('Accepted POST without requests (notifications only)');
259
+ return;
260
+ }
261
+
262
+ const orderedIds: RequestId[] = messages.filter(isJSONRPCRequest).map((m) => m.id);
263
+ const sseConnected = !!this.sse && !this.sse.res.writableEnded;
264
+ // Prefer JSON response when client explicitly accepts JSON; use SSE only when
265
+ // Accept doesn't include JSON and an SSE stream is connected
266
+ if (sseConnected && !acceptsJson) {
267
+ // With SSE, we emit responses on the SSE stream; reply 202 to POST immediately
268
+ res.writeHead(202).end();
269
+ for (const msg of messages) {
270
+ this.onmessage?.(msg, {
271
+ requestInfo: {
272
+ headers: req.headers,
273
+ method: req.method,
274
+ transport: 'sse',
275
+ },
276
+ authInfo: req.auth,
277
+ });
278
+ }
279
+ logger.info('POST handled with SSE active: responded 202 and streaming via SSE');
280
+ return;
281
+ }
282
+
283
+ const connId = `${Date.now()}-${Math.random()}`;
284
+ this.connections.set(connId, {
285
+ res,
286
+ orderedIds,
287
+ pendingIds: new Set(orderedIds),
288
+ responses: new Map(),
289
+ });
290
+ orderedIds.forEach((id) => this.requestIdToConn.set(id, connId));
291
+
292
+ res.on('close', () => {
293
+ this.connections.delete(connId);
294
+ orderedIds.forEach((id) => this.requestIdToConn.delete(id));
295
+ });
296
+
297
+ for (const msg of messages) {
298
+ this.onmessage?.(msg, {
299
+ requestInfo: {
300
+ headers: req.headers,
301
+ method: req.method,
302
+ transport: 'http',
303
+ },
304
+ authInfo: req.auth,
305
+ });
306
+ }
307
+ logger.info({ requestIds: orderedIds }, 'POST handled with JSON response mode');
308
+ } catch (error) {
309
+ this.onerror?.(error as Error);
310
+ res.writeHead(400);
311
+ res.end(
312
+ JSON.stringify({
313
+ jsonrpc: '2.0',
314
+ error: {
315
+ code: -32700,
316
+ message: 'Parse error',
317
+ data: String(error),
318
+ },
319
+ id: null,
320
+ }),
321
+ );
322
+ logger.error(error as Error, 'HTTP request handling parse/validation error');
323
+ }
324
+ }
325
+ }
@@ -0,0 +1,58 @@
1
+ import LinkedApi, { Operation, TMappedResponse, TOperationName } from '@linkedapi/node';
2
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
3
+ import { LinkedApiProgressNotification } from 'src/utils/types';
4
+ import z from 'zod';
5
+
6
+ import { executeWithProgress } from './execute-with-progress';
7
+
8
+ export abstract class LinkedApiTool<TParams, TResult> {
9
+ public abstract readonly name: string;
10
+ protected abstract readonly schema: z.ZodSchema;
11
+ protected readonly progressCallback: (progress: LinkedApiProgressNotification) => void;
12
+
13
+ constructor(progressCallback: (progress: LinkedApiProgressNotification) => void) {
14
+ this.progressCallback = progressCallback;
15
+ }
16
+
17
+ public abstract getTool(): Tool;
18
+
19
+ public validate(args: unknown): TParams {
20
+ return this.schema.parse(args) as TParams;
21
+ }
22
+
23
+ public abstract execute({
24
+ linkedapi,
25
+ args,
26
+ workflowTimeout,
27
+ progressToken,
28
+ }: {
29
+ linkedapi: LinkedApi;
30
+ args: TParams;
31
+ workflowTimeout: number;
32
+ progressToken?: string | number;
33
+ }): Promise<TMappedResponse<TResult>>;
34
+ }
35
+
36
+ export abstract class OperationTool<TParams, TResult> extends LinkedApiTool<TParams, TResult> {
37
+ public abstract readonly operationName: TOperationName;
38
+
39
+ public override execute({
40
+ linkedapi,
41
+ args,
42
+ workflowTimeout,
43
+ progressToken,
44
+ }: {
45
+ linkedapi: LinkedApi;
46
+ args: TParams;
47
+ workflowTimeout: number;
48
+ progressToken?: string | number;
49
+ }): Promise<TMappedResponse<TResult>> {
50
+ const operation = linkedapi.operations.find(
51
+ (operation) => operation.operationName === this.operationName,
52
+ )! as Operation<TParams, TResult>;
53
+ return executeWithProgress(this.progressCallback, operation, workflowTimeout, {
54
+ params: args,
55
+ progressToken,
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,84 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ const levelPriority: Record<LogLevel, number> = {
4
+ debug: 10,
5
+ info: 20,
6
+ warn: 30,
7
+ error: 40,
8
+ };
9
+
10
+ function getMinLevel(): LogLevel {
11
+ const env = (process.env.LOG_LEVEL || '').toLowerCase();
12
+ if (env === 'debug' || env === 'info' || env === 'warn' || env === 'error') return env;
13
+ return 'info';
14
+ }
15
+
16
+ const minLevel = getMinLevel();
17
+
18
+ function shouldLog(level: LogLevel): boolean {
19
+ return levelPriority[level] >= levelPriority[minLevel];
20
+ }
21
+
22
+ function toErrorObject(value: unknown) {
23
+ if (value instanceof Error) {
24
+ return {
25
+ name: value.name,
26
+ message: value.message,
27
+ stack: value.stack,
28
+ };
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ function writeLine(line: string) {
34
+ process.stderr.write(line + '\n');
35
+ }
36
+
37
+ function baseLog(level: LogLevel, arg1?: unknown, arg2?: unknown) {
38
+ if (!shouldLog(level)) return;
39
+
40
+ let message: string | undefined;
41
+ let payload: unknown | undefined;
42
+
43
+ if (typeof arg1 === 'string' && typeof arg2 === 'undefined') {
44
+ message = arg1;
45
+ } else if (typeof arg2 === 'string') {
46
+ message = arg2;
47
+ if (arg1 && typeof arg1 === 'object') {
48
+ const errObj = toErrorObject(arg1);
49
+ payload = errObj ?? arg1;
50
+ } else if (arg1 instanceof Error) {
51
+ payload = toErrorObject(arg1);
52
+ }
53
+ } else if (arg1 instanceof Error) {
54
+ payload = toErrorObject(arg1);
55
+ } else if (arg1 && typeof arg1 === 'object') {
56
+ payload = arg1 as Record<string, unknown>;
57
+ } else if (typeof arg1 !== 'undefined') {
58
+ message = String(arg1);
59
+ }
60
+
61
+ if (typeof message === 'string') {
62
+ writeLine(message);
63
+ }
64
+
65
+ if (typeof payload !== 'undefined') {
66
+ try {
67
+ // Pretty-print JSON payload without internal fields like time/level
68
+ const pretty = JSON.stringify(payload, null, 2);
69
+ writeLine(pretty);
70
+ } catch {
71
+ // Fallback to toString if payload is not serializable
72
+ writeLine(String(payload));
73
+ }
74
+ }
75
+ }
76
+
77
+ export const logger = {
78
+ debug: (arg1?: unknown, arg2?: unknown) => baseLog('debug', arg1, arg2),
79
+ info: (arg1?: unknown, arg2?: unknown) => baseLog('info', arg1, arg2),
80
+ warn: (arg1?: unknown, arg2?: unknown) => baseLog('warn', arg1, arg2),
81
+ error: (arg1?: unknown, arg2?: unknown) => baseLog('error', arg1, arg2),
82
+ };
83
+
84
+ export type Logger = typeof logger;
@@ -0,0 +1,17 @@
1
+ import { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ export type { CallToolResult };
4
+
5
+ export interface ExtendedCallToolRequest extends CallToolRequest {
6
+ params: CallToolRequest['params'] & {
7
+ _meta?: {
8
+ progressToken?: string | number;
9
+ };
10
+ };
11
+ }
12
+ export interface LinkedApiProgressNotification {
13
+ progressToken?: string | number;
14
+ progress: number;
15
+ total?: number;
16
+ message?: string;
17
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "moduleResolution": "node",
5
+ "module": "commonjs",
6
+ "composite": false,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "allowUnreachableCode": false,
15
+ "strictNullChecks": true,
16
+ "allowUnusedLabels": false,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noImplicitOverride": true,
19
+ "noImplicitReturns": true,
20
+ "noUncheckedIndexedAccess": true,
21
+ "noUnusedLocals": false,
22
+ "resolveJsonModule": true,
23
+ "removeComments": false,
24
+ "emitDecoratorMetadata": true,
25
+ "experimentalDecorators": true,
26
+ "allowSyntheticDefaultImports": true,
27
+ "outDir": "./dist",
28
+ "baseUrl": "./",
29
+ "incremental": true,
30
+ "jsx": "react-jsx",
31
+ "allowArbitraryExtensions": true,
32
+ "typeRoots": ["./node_modules/@types", "./types"]
33
+ },
34
+ "exclude": ["**/node_modules/*", "**/build/*", "**/dist/*"],
35
+ "include": ["src/**/*", "types/**/*"]
36
+ }