Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "1.0.0",
"configurations": [
{
"type": "node-terminal",
"name": "Linked API MCP - HTTP",
"request": "launch",
"command": "npm run dev:http",
"envFile": "${workspaceFolder}/.env"
},
{
"type": "node-terminal",
"name": "Linked API MCP - STDIO",
"request": "launch",
"command": "npm run dev:stdio",
"envFile": "${workspaceFolder}/.env"
},
]
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "linkedapi-mcp",
"version": "0.3.1",
"version": "0.3.2",
"description": "MCP server for Linked API",
"main": "dist/index.js",
"bin": {
Expand All @@ -10,6 +10,8 @@
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"dev:http": "tsx src/index.ts --http",
"dev:stdio": "tsx src/index.ts",
"start": "node dist/index.js",
"watch": "tsx watch src/index.ts",
"clean": "rm -rf dist",
Expand Down
52 changes: 32 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import http from 'node:http';

import { LinkedApiMCPServer } from './linked-api-server';
import { availablePrompts, getPromptContent, systemPrompt } from './prompts';
import { debugLog } from './utils/debug-log';
import { JsonHTTPServerTransport } from './utils/json-http-transport';
import { logger } from './utils/logger';
import { LinkedApiProgressNotification } from './utils/types';

function getArgValue(flag: string): string | undefined {
Expand Down Expand Up @@ -83,11 +83,7 @@ async function main() {
});

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
debugLog('Tool request received', {
toolName: request.params.name,
arguments: request.params.arguments,
progressToken: request.params._meta?.progressToken,
});
logger.info('Tool request received');

try {
const localLinkedApiToken = process.env.LINKED_API_TOKEN;
Expand All @@ -97,18 +93,30 @@ async function main() {
const identificationToken = (headers['identification-token'] ??
localIdentificationToken ??
'') as string;
const mcpClient = (headers['client'] ?? '') as string;

const result = await linkedApiServer.executeWithTokens(request.params, {
linkedApiToken,
identificationToken,
mcpClient,
});
return result;
} catch (error) {
debugLog('Tool execution failed', {
toolName: request.params.name,
error: error instanceof Error ? error.message : String(error),
});
throw error;
logger.error(
{
toolName: request.params.name,
error: error instanceof Error ? error.message : String(error),
},
'Critical tool execution error',
);
return {
content: [
{
type: 'text',
text: 'Unknown error. Please try again.',
},
],
};
}
});

Expand All @@ -130,36 +138,40 @@ async function main() {
// Set query parameters to headers if they are not set
const linkedApiTokenQP = url.searchParams.get('linked-api-token');
const identificationTokenQP = url.searchParams.get('identification-token');
const mcpClient = url.searchParams.get('client');
if (!req.headers['linked-api-token'] && linkedApiTokenQP) {
req.headers['linked-api-token'] = linkedApiTokenQP;
}
if (!req.headers['identification-token'] && identificationTokenQP) {
req.headers['identification-token'] = identificationTokenQP;
}
if (!req.headers['client'] && mcpClient) {
req.headers['client'] = mcpClient;
}
await transport.handleRequest(req, res);
} catch (error) {
debugLog('HTTP request handling failed', {
error: error instanceof Error ? error.message : String(error),
});
logger.error(
{
error: error instanceof Error ? error.message : String(error),
},
'HTTP request handling failed',
);
res.statusCode = 500;
res.end('Internal Server Error');
}
});

httpServer.listen(port, host, () => {
debugLog('HTTP transport listening', {
host,
port,
});
logger.info({ host }, `HTTP transport listening on port ${port}`);
});
} else {
const transport = new StdioServerTransport();
await server.connect(transport);
debugLog('stdio transport connected');
logger.info('stdio transport connected');
}
}

main().catch((error) => {
debugLog('Fatal error', error);
logger.error(error, 'Fatal error');
process.exit(1);
});
80 changes: 67 additions & 13 deletions src/linked-api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { LinkedApi, LinkedApiError, TLinkedApiConfig } from 'linkedapi-node';
import { buildLinkedApiHttpClient } from 'linkedapi-node/dist/core';

import { LinkedApiTools } from './linked-api-tools';
import { debugLog } from './utils/debug-log';
import { defineRequestTimeoutInSeconds } from './utils/define-request-timeout';
import { handleLinkedApiError } from './utils/handle-linked-api-error';
import { logger } from './utils/logger';
import {
CallToolResult,
ExtendedCallToolRequest,
Expand All @@ -24,26 +25,52 @@ export class LinkedApiMCPServer {

public async executeWithTokens(
request: ExtendedCallToolRequest['params'],
config: TLinkedApiConfig,
{ linkedApiToken, identificationToken, mcpClient }: TLinkedApiConfig & { mcpClient: string },
): Promise<CallToolResult> {
const linkedApi = new LinkedApi(
const workflowTimeout = defineRequestTimeoutInSeconds(mcpClient) * 1000;
logger.info(
{
toolName: request.name,
arguments: request.arguments,
mcpClient,
workflowTimeout,
},
'Tool execution started',
);
const linkedapi = new LinkedApi(
buildLinkedApiHttpClient(
{
linkedApiToken: config.linkedApiToken!,
identificationToken: config.identificationToken!,
linkedApiToken: linkedApiToken,
identificationToken: identificationToken,
},
'mcp',
),
);

const { name, arguments: args, _meta } = request;
const { name: toolName, arguments: args, _meta } = request;
const progressToken = _meta?.progressToken;

const startTime = Date.now();
try {
const tool = this.tools.toolByName(name)!;
const tool = this.tools.toolByName(toolName)!;
const params = tool.validate(args);
const { data, errors } = await tool.execute(linkedApi, params, progressToken);
const { data, errors } = await tool.execute({
linkedapi,
args: params,
workflowTimeout,
progressToken,
});
const endTime = Date.now();
const duration = `${((endTime - startTime) / 1000).toFixed(2)} seconds`;
if (errors.length > 0 && !data) {
logger.error(
{
toolName,
duration,
errors,
},
'Tool execution failed',
);
return {
content: [
{
Expand All @@ -53,6 +80,14 @@ export class LinkedApiMCPServer {
],
};
}
logger.info(
{
toolName,
duration,
data,
},
'Tool execution successful',
);
if (data) {
return {
content: [
Expand All @@ -72,8 +107,17 @@ export class LinkedApiMCPServer {
],
};
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof LinkedApiError) {
const body = handleLinkedApiError(error);
logger.error(
{
toolName,
duration,
body,
},
'Tool execution failed with Linked API error',
);
return {
content: [
{
Expand All @@ -84,19 +128,29 @@ export class LinkedApiMCPServer {
};
}
const errorMessage = error instanceof Error ? error.message : String(error);
debugLog(`Tool ${name} execution failed`, {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
});
logger.error(
{
toolName,
duration,
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
},
'Tool execution failed with unknown error',
);

return {
content: [
{
type: 'text' as const,
text: `Error executing ${name}: ${errorMessage}`,
text: `Error executing ${toolName}: ${errorMessage}`,
},
],
};
}
}

private calculateDuration(startTime: number): string {
const endTime = Date.now();
return `${((endTime - startTime) / 1000).toFixed(2)} seconds`;
}
}
17 changes: 11 additions & 6 deletions src/tools/get-api-usage-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ export class GetApiUsageTool extends LinkedApiTool<TApiUsageParams, TApiUsageAct
end: z.string(),
});

public override async execute(
linkedapi: LinkedApi,
args: TApiUsageParams,
): Promise<TMappedResponse<TApiUsageAction[]>> {
public override async execute({
linkedapi,
args,
}: {
linkedapi: LinkedApi;
args: TApiUsageParams;
workflowTimeout: number;
progressToken?: string | number;
}): Promise<TMappedResponse<TApiUsageAction[]>> {
return await linkedapi.getApiUsage(args);
}

public override getTool(): Tool {
return {
name: this.name,
description: 'Retrieve Linked API usage statistics',
description: 'Retrieve Linked API usage statistics. Date range must not exceed 30 days.',
inputSchema: {
type: 'object',
properties: {
Expand All @@ -33,7 +38,7 @@ export class GetApiUsageTool extends LinkedApiTool<TApiUsageParams, TApiUsageAct
end: {
type: 'string',
description:
"End date for the statistics period in ISO 8601 format (e.g., '2024-01-31T23:59:59Z')",
"End date for the statistics period in ISO 8601 format (e.g., '2024-01-30T00:00:00Z')",
},
},
required: ['start', 'end'],
Expand Down
19 changes: 9 additions & 10 deletions src/tools/get-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ export class GetConversationTool extends LinkedApiTool<
since: z.string().optional(),
});

public override async execute(
linkedapi: LinkedApi,
{
personUrl,
since,
}: {
personUrl: string;
since?: string;
},
): Promise<TMappedResponse<TConversationPollResult>> {
public override async execute({
linkedapi,
args: { personUrl, since },
}: {
linkedapi: LinkedApi;
args: { personUrl: string; since?: string };
workflowTimeout: number;
progressToken?: string | number;
}): Promise<TMappedResponse<TConversationPollResult>> {
const conversations = await this.getConversation(linkedapi, personUrl, since);
if (conversations.errors.length === 0) {
return conversations;
Expand Down
22 changes: 14 additions & 8 deletions src/tools/get-workflow-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ export class GetWorkflowResultTool extends LinkedApiTool<IGetWorkflowResultParam
operationName: z.enum(Object.values(OPERATION_NAME)),
});

public override async execute(
linkedapi: LinkedApi,
args: IGetWorkflowResultParams,
progressToken?: string | number,
): Promise<TMappedResponse<unknown>> {
public override async execute({
linkedapi,
args: { workflowId, operationName },
workflowTimeout,
progressToken,
}: {
linkedapi: LinkedApi;
args: IGetWorkflowResultParams;
workflowTimeout: number;
progressToken?: string | number;
}): Promise<TMappedResponse<unknown>> {
const operation = linkedapi.operations.find(
(operation) => operation.operationName === args.operationName,
(operation) => operation.operationName === operationName,
)!;
return await executeWithProgress(this.progressCallback, operation, {
workflowId: args.workflowId,
return await executeWithProgress(this.progressCallback, operation, workflowTimeout, {
workflowId,
progressToken,
});
}
Expand Down
Loading