diff --git a/mcp-server/src/connection.ts b/mcp-server/src/connection.ts index f9d62f3..799489d 100644 --- a/mcp-server/src/connection.ts +++ b/mcp-server/src/connection.ts @@ -1,6 +1,7 @@ import { SDKNodePlatform } from '@amp-labs/sdk-node-platform'; import { z } from 'zod'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { logger } from '@amp-labs/ai/mcp'; import { providerSchema } from './schemas'; import { ClientSettings } from '.'; @@ -27,13 +28,13 @@ export async function createConnectionManagerTools( provider: provider, }; const data = await ampersandClient.connections.list(body); - console.log('[CHECK-CONNECTION] API call to listConnections: ', body); + logger.info('[CHECK-CONNECTION] API call to listConnections: ', body); // @ts-ignore if (data.length > 0) { // @ts-ignore const connection = data[0]; - console.log( + logger.info( '[CHECK-CONNECTION] API response from listConnections:', connection, ); @@ -56,7 +57,7 @@ export async function createConnectionManagerTools( }; } } catch (err) { - console.error('Error checking connection:', err); + logger.error('[CHECK-CONNECTION] Error checking connection:', err); return { content: [ { @@ -90,13 +91,13 @@ export async function createConnectionManagerTools( process.env.AMPERSAND_INTEGRATION_NAME || '', }; - console.log( + logger.info( '[CHECK-INSTALLATION] API call to listInstallations: ', body, ); const data = await ampersandClient.installations.list(body); - console.log( + logger.info( '[CHECK-INSTALLATION] API response from listInstallations: ', data, ); @@ -105,7 +106,7 @@ export async function createConnectionManagerTools( // @ts-ignore (inst) => inst.connection?.provider === provider.toLowerCase(), ); - console.log( + logger.info( '[CHECK-INSTALLATION] filtered installations: ', relevantInstallations, ); @@ -135,7 +136,7 @@ export async function createConnectionManagerTools( }; } } catch (err) { - console.error('Error checking installation:', err); + logger.error('[CHECK-INSTALLATION] Error checking installation:', err); return { content: [ { @@ -182,7 +183,7 @@ export async function createConnectionManagerTools( }, }, }; - console.log( + logger.info( '[CREATE-INSTALLATION] API call to createInstallation: ', requestBody, ); @@ -197,7 +198,7 @@ export async function createConnectionManagerTools( requestBody, }); - console.log( + logger.info( '[CREATE-INSTALLATION] API response from createInstallation: ', data, ); @@ -222,7 +223,7 @@ export async function createConnectionManagerTools( ], }; } catch (err) { - console.error('Error creating installation:', err); + logger.error('[CREATE-INSTALLATION] Error creating installation:', err); return { content: [ { @@ -257,11 +258,11 @@ export async function ensureInstallation( settings?.project || process.env.AMPERSAND_PROJECT_ID || '', provider: provider, }; - console.log('[ENSURE-INSTALLATION] API call to listConnections: ', body); + logger.info('[ENSURE-INSTALLATION] API call to listConnections: ', body); const connectionData = await ampersandClient.connections.list(body); - console.log( + logger.info( '[ENSURE-INSTALLATION] API response from listConnections: ', connectionData, ); @@ -300,18 +301,15 @@ export async function ensureInstallation( inst.connection?.id === connectionId, ); - console.log( - '[ENSURE-INSTALLATION] existing installation check', + logger.info('[ENSURE-INSTALLATION] existing installation check', { relevantInstallations, installationData, - ); + }); if (relevantInstallations.length === 0) { - console.log( + logger.info( '[ENSURE-INSTALLATION] No existing installation found, creating one for connection:', - connectionId, - 'group:', - groupRef, + { connectionId, groupRef }, ); const requestBody = { @@ -326,7 +324,7 @@ export async function ensureInstallation( }, }; - console.log( + logger.info( '[ENSURE-INSTALLATION] API request to createInstallation', requestBody, ); @@ -340,7 +338,7 @@ export async function ensureInstallation( requestBody, }); - console.log( + logger.info( '[ENSURE-INSTALLATION] API response from createInstallation', createData, ); @@ -348,7 +346,7 @@ export async function ensureInstallation( // @ts-ignore const installationId = createData.installation?.id; if (installationId) { - console.log( + logger.info( `[ENSURE-INSTALLATION]Installation created for ${provider}, Installation ID: ${installationId}`, ); return installationId; @@ -358,7 +356,7 @@ export async function ensureInstallation( ); } } else { - console.log( + logger.info( `[ENSURE-INSTALLATION]Installation already exists for ${provider} with ID: ${relevantInstallations[0].id}`, ); return relevantInstallations[0].id; diff --git a/mcp-server/src/oauth.ts b/mcp-server/src/oauth.ts index 805d2f5..8ffeec9 100644 --- a/mcp-server/src/oauth.ts +++ b/mcp-server/src/oauth.ts @@ -1,4 +1,5 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { logger } from '@amp-labs/ai/mcp'; import { providerSchema } from './schemas'; import { ClientSettings } from '.'; import crypto from 'crypto'; @@ -29,7 +30,7 @@ export async function createStartOAuthTool( }, body: JSON.stringify({ provider, consumerRef, groupRef, projectId }), }; - console.log( + logger.info( '[START-OAUTH] API request to oauthConnect: ', options.body, ); @@ -39,10 +40,10 @@ export async function createStartOAuthTool( options, ); const data = await response.text(); - console.log('[START-OAUTH] API response from oauthConnect: ', data); + logger.info('[START-OAUTH] API response from oauthConnect: ', data); oAuthUrl = data; } catch (err) { - console.error(err); + logger.error('[START-OAUTH] Error in oauthConnect', err); } return { diff --git a/mcp-server/src/request.ts b/mcp-server/src/request.ts index ceaf9f5..922ebcb 100644 --- a/mcp-server/src/request.ts +++ b/mcp-server/src/request.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { logger } from '@amp-labs/ai/mcp'; import { ensureInstallation } from './connection'; import { endpointSchema, @@ -131,12 +132,11 @@ async function callAmpersandProxy({ fetchOptions.body = JSON.stringify(body); } - console.log( - '[SEND-REQUEST] API request to proxy: ', + logger.info('[SEND-REQUEST] API request to proxy: ', { endpoint, - fetchOptions.body, - ); - console.log( + body: fetchOptions.body, + }); + logger.info( '[SEND-REQUEST] Full URL: ', `https://proxy.withampersand.com/${endpoint}`, ); @@ -146,11 +146,10 @@ async function callAmpersandProxy({ fetchOptions, ); - console.log( - '[SEND-REQUEST] API response status from proxy: ', + logger.info('[SEND-REQUEST] API response status from proxy: ', { endpoint, - response.status, - ); + status: response.status, + }); const data = await response.text(); return { @@ -170,8 +169,8 @@ async function callAmpersandProxy({ ], }; } catch (error) { - console.error( - `Error in ${method === 'GET' ? 'sendReadRequest' : 'sendRequest'} tool`, + logger.error( + `[SEND-REQUEST] Error in ${method === 'GET' ? 'sendReadRequest' : 'sendRequest'} tool`, error, ); return { diff --git a/mcp-server/src/search.ts b/mcp-server/src/search.ts index 6fd8207..fd62499 100644 --- a/mcp-server/src/search.ts +++ b/mcp-server/src/search.ts @@ -1,6 +1,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { TrieveSDK, ChunkMetadata } from 'trieve-ts-sdk'; import { z } from 'zod'; +import { logger } from '@amp-labs/ai/mcp'; export const SUBDOMAIN: string = 'ampersand-24eb5c1a'; export const SERVER_URL: string = 'https://leaves.mintlify.com'; @@ -96,7 +97,7 @@ export async function createSearchTool(server: Server): Promise { query: z.string(), }, async ({ query }: { query: string }) => { - console.log('[SEARCH] call: ', query); + logger.info('[SEARCH] call: ', query); const results = await search(query, config); const content = results.map((result) => { const { title, content, link } = result; @@ -113,6 +114,6 @@ export async function createSearchTool(server: Server): Promise { }, ); } catch (error) { - console.warn('Error in registering search tool:', error); + logger.warn('[SEARCH] Error in registering search tool:', error); } } diff --git a/mcp-server/src/session.ts b/mcp-server/src/session.ts index c9d6acd..1602969 100644 --- a/mcp-server/src/session.ts +++ b/mcp-server/src/session.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import express, { Request, Response } from 'express'; import { detect } from 'detect-port'; +import { logger } from '@amp-labs/ai/mcp'; const DEFAULT_PORT = 3001; const SSE_SERVER_VERSION = 'v1'; @@ -50,7 +51,7 @@ export async function connectServer( settings: any, ): Promise { if (useStdioTransport) { - console.log('Connecting to MCP server over stdio'); + logger.info('Connecting to MCP server over stdio'); const transport = new StdioServerTransport(); await server.connect(transport); return; @@ -85,7 +86,7 @@ export async function connectServer( // VS code supports it for example like this: https://github.com/microsoft/vscode-docs/blob/74c4fd5aa3180b218fc389184659b621f05460ca/docs/copilot/chat/mcp-servers.md#configuration-example settings.apiKey = req.headers['x-api-key']; } - console.log('[SESSION] Settings: ', settings); + logger.info('[SESSION] Settings: ', settings); currentTransport = new SSEServerTransport('/messages', res); transportManager.addTransport(currentTransport, res); await server.connect(currentTransport); @@ -94,7 +95,7 @@ export async function connectServer( app.post('/messages', async (req: Request, res: Response) => { const sessionId = req.query.sessionId as string; - console.log('[SESSION] Session ID', sessionId); + logger.info('[SESSION] Session ID', sessionId); if (!sessionId) { res.status(400).json({ error: 'Missing session ID param' }); return; diff --git a/sdk/lib/adapters/ampersand/core/write.ts b/sdk/lib/adapters/ampersand/core/write.ts index 65b7688..f61e13d 100644 --- a/sdk/lib/adapters/ampersand/core/write.ts +++ b/sdk/lib/adapters/ampersand/core/write.ts @@ -1,5 +1,6 @@ import { WriteResponse } from '../types'; import * as Sentry from '@sentry/node'; + interface WriteParams { objectName: string; type: 'create' | 'update'; diff --git a/sdk/lib/adapters/mcp.ts b/sdk/lib/adapters/mcp.ts index 2b1e3d8..9dee999 100644 --- a/sdk/lib/adapters/mcp.ts +++ b/sdk/lib/adapters/mcp.ts @@ -6,6 +6,7 @@ import './ampersand/core/instrument'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { z } from 'zod'; import * as Sentry from '@sentry/node'; +import { logger } from '../logger'; import { providerSchema, associationsSchema, @@ -33,6 +34,9 @@ import { ensureInstallationExists, } from './common'; +// Re-export logger for use by mcp-server package +export { logger }; + type MCPResponse = { content: Array<{ type: string; text: string }>; isError?: boolean; @@ -83,7 +87,7 @@ export const createWriteActionTool = async ( ): Promise => { const { objectName, type, record, groupRef, associations } = params; - console.log(`[WRITE] about to perform ${type} operation`, params); + logger.info(`[WRITE] about to perform ${type} operation`, params); const result = await executeAmpersandWrite({ objectName, @@ -100,7 +104,7 @@ export const createWriteActionTool = async ( }); if (result.success) { - console.log( + logger.info( `[WRITE] ${type} operation on provider succeeded:`, result.response, ); diff --git a/sdk/lib/logger.ts b/sdk/lib/logger.ts new file mode 100644 index 0000000..336a52f --- /dev/null +++ b/sdk/lib/logger.ts @@ -0,0 +1,49 @@ +/** + * Logger utility for SDK. + * Uses stderr (console.error) to avoid interfering with stdio transport JSON-RPC protocol. + * All logs are captured by Claude Desktop and written to logs. + */ + +type LogLevel = 'INFO' | 'ERROR' | 'WARN' | 'DEBUG'; + +function formatLog(level: LogLevel, message: string, data?: any): string { + const timestamp = new Date().toISOString(); + const baseMessage = `[${level}] ${timestamp} ${message}`; + + if (data !== undefined) { + if (typeof data === 'object') { + // Handle Error objects to preserve stack traces + if (data instanceof Error) { + return `${baseMessage} ${data.stack || data.message}`; + } + // Wrap JSON.stringify in try-catch to handle circular references + try { + return `${baseMessage} ${JSON.stringify(data)}`; + } catch (error) { + // Fallback for circular references or other stringify errors + return `${baseMessage} [Object with circular reference or unserializable content]`; + } + } + return `${baseMessage} ${data}`; + } + + return baseMessage; +} + +export const logger = { + info: (message: string, data?: any) => { + console.error(formatLog('INFO', message, data)); + }, + + error: (message: string, error?: any) => { + console.error(formatLog('ERROR', message, error)); + }, + + warn: (message: string, data?: any) => { + console.error(formatLog('WARN', message, data)); + }, + + debug: (message: string, data?: any) => { + console.error(formatLog('DEBUG', message, data)); + }, +};