Skip to content
Closed
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
506 changes: 472 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,23 @@
"test:timezones": "node scripts/test-timezones.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
"@modelcontextprotocol/sdk": "^1.22.0",
"@toon-format/toon": "^1.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/node": "^20.0.0",
"cors": "^2.8.5",
"express": "^4.21.2",
"node-fetch": "^3.3.2",
"zod": "^3.25.0",
"typescript": "^5.3.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/node": "^20.0.0"
"zod": "^3.25.0"
},
"devDependencies": {
"@babel/parser": "^7.27.5",
"@babel/traverse": "^7.27.4",
"@babel/types": "^7.27.6",
"@types/babel__traverse": "^7.20.7",



"tsx": "^4.7.0"
"tsx": "^4.21.0"
},
"engines": {
"node": ">=18.0.0"
Expand All @@ -72,4 +70,4 @@
"compatible": true,
"version": "0.1"
}
}
}
181 changes: 181 additions & 0 deletions src/config/tool-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Tool Pagination Configuration
*
* Implements MCP 2025-06-18 tool pagination support for lazy loading.
* This allows clients to load tools incrementally rather than all at once,
* significantly reducing initial context window overhead.
*
* Per MCP spec:
* - tools/list supports cursor-based pagination
* - Servers can return tools in chunks with nextCursor
* - Clients use cursor parameter to fetch next page
*/

export interface ToolPaginationConfig {
/**
* Number of tools to return per page
* Default: 10 (optimized for context window efficiency)
* Set to 0 or Infinity to disable pagination (return all tools)
*/
pageSize: number;

/**
* Enable pagination (default: true for context efficiency)
* Set to false for backward compatibility with older clients
*/
enabled: boolean;
}

/**
* Default pagination configuration
*
* DISABLED BY DEFAULT: Most MCP clients (Cursor, Claude Desktop) don't support
* tool pagination yet. Enable via TOOL_PAGINATION_ENABLED=true when clients
* support the MCP 2025-06-18 pagination spec.
*
* When enabled, returns 10 tools per page for optimal context window usage.
*/
export const DEFAULT_TOOL_PAGINATION: ToolPaginationConfig = {
pageSize: 10,
enabled: false // Disabled until clients support pagination
};

/**
* Environment-based pagination config
*
* Can be overridden via environment variables:
* - TOOL_PAGINATION_ENABLED: 'true' or 'false'
* - TOOL_PAGINATION_PAGE_SIZE: number of tools per page
*/
export function getToolPaginationConfig(): ToolPaginationConfig {
const envEnabled = process.env.TOOL_PAGINATION_ENABLED;
const envPageSize = process.env.TOOL_PAGINATION_PAGE_SIZE;

return {
enabled: envEnabled !== undefined ? envEnabled === 'true' : DEFAULT_TOOL_PAGINATION.enabled,
pageSize: envPageSize ? parseInt(envPageSize, 10) : DEFAULT_TOOL_PAGINATION.pageSize
};
}

/**
* Pagination cursor utilities
*/
export class ToolPaginationCursor {
/**
* Encode pagination state to cursor string
*/
static encode(startIndex: number): string {
return Buffer.from(JSON.stringify({ s: startIndex })).toString('base64url');
}

/**
* Decode cursor string to pagination state
*/
static decode(cursor: string): { startIndex: number } | null {
try {
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
if (typeof decoded.s === 'number') {
return { startIndex: decoded.s };
}
return null;
} catch {
return null;
}
}
}

/**
* Paginate tools array
*
* @param tools - Full array of tool definitions
* @param cursor - Optional cursor from previous request
* @param config - Pagination configuration
* @returns Paginated result with tools and nextCursor
*/
export function paginateTools<T>(
tools: T[],
cursor?: string,
config: ToolPaginationConfig = getToolPaginationConfig()
): { tools: T[]; nextCursor?: string } {
// If pagination disabled, return all tools
if (!config.enabled || config.pageSize <= 0 || config.pageSize >= tools.length) {
return { tools };
}

// Decode cursor to get start index
let startIndex = 0;
if (cursor) {
const decoded = ToolPaginationCursor.decode(cursor);
if (decoded) {
startIndex = decoded.startIndex;
}
}

// Validate start index
if (startIndex < 0 || startIndex >= tools.length) {
startIndex = 0;
}

// Calculate end index
const endIndex = Math.min(startIndex + config.pageSize, tools.length);
const pageTools = tools.slice(startIndex, endIndex);

// Generate next cursor if more tools available
const hasMore = endIndex < tools.length;
const nextCursor = hasMore ? ToolPaginationCursor.encode(endIndex) : undefined;

return {
tools: pageTools,
nextCursor
};
}

/**
* Get pagination metadata for logging/debugging
*/
export function getPaginationInfo(
totalTools: number,
cursor?: string,
config: ToolPaginationConfig = getToolPaginationConfig()
): {
totalTools: number;
pageSize: number;
currentPage: number;
totalPages: number;
startIndex: number;
endIndex: number;
} {
if (!config.enabled || config.pageSize <= 0) {
return {
totalTools,
pageSize: totalTools,
currentPage: 1,
totalPages: 1,
startIndex: 0,
endIndex: totalTools
};
}

let startIndex = 0;
if (cursor) {
const decoded = ToolPaginationCursor.decode(cursor);
if (decoded) {
startIndex = decoded.startIndex;
}
}

const pageSize = config.pageSize;
const totalPages = Math.ceil(totalTools / pageSize);
const currentPage = Math.floor(startIndex / pageSize) + 1;
const endIndex = Math.min(startIndex + pageSize, totalTools);

return {
totalTools,
pageSize,
currentPage,
totalPages,
startIndex,
endIndex
};
}

42 changes: 11 additions & 31 deletions src/handlers/lead-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { makeInstantlyRequest } from '../api/client.js';
import { ENDPOINTS } from '../api/endpoints.js';
import { createMCPResponse } from '../utils/response-formatter.js';

/**
* Handle all lead-related tool executions
Expand Down Expand Up @@ -167,14 +168,7 @@ async function handleListLeads(args: any, apiKey: string) {
response.client_side_filters_applied = filtersApplied;
}

return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
return createMCPResponse(response);
} catch (error: any) {
const elapsed = Date.now() - startTime;
console.error(`[Instantly MCP] ❌ Request failed after ${elapsed}ms: ${error.message}`);
Expand All @@ -196,14 +190,7 @@ async function handleGetLead(args: any, apiKey: string) {

const result = await makeInstantlyRequest(`/leads/${args.lead_id}`, {}, apiKey);

return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
return createMCPResponse(result);
}

/**
Expand Down Expand Up @@ -503,21 +490,14 @@ async function handleListLeadLists(args: any, apiKey: string) {
const items = listsResult.items || listsResult;
const nextStartingAfter = listsResult.next_starting_after;

return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
lead_lists: items,
next_starting_after: nextStartingAfter,
total_returned: Array.isArray(items) ? items.length : 0,
has_more: !!nextStartingAfter,
message: 'Lead lists retrieved successfully'
}, null, 2)
}
]
};
return createMCPResponse({
success: true,
lead_lists: items,
next_starting_after: nextStartingAfter,
total_returned: Array.isArray(items) ? items.length : 0,
has_more: !!nextStartingAfter,
message: 'Lead lists retrieved successfully'
});
}

/**
Expand Down
26 changes: 22 additions & 4 deletions src/handlers/mcp-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { TOOLS_DEFINITION } from '../tools/index.js';
import { executeToolDirectly } from './tool-executor.js';
import { handleInstantlyError } from '../error-handler.js';
import { paginateTools, getPaginationInfo, getToolPaginationConfig } from '../config/tool-pagination.js';

/**
* Load Instantly.ai icons for MCP protocol
Expand Down Expand Up @@ -103,12 +104,29 @@ export function registerMcpHandlers(server: Server, apiKey?: string): void {
return initResponse;
});

// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.error('[Instantly MCP] 📋 Listing available tools...');
// List tools handler with pagination support (MCP 2025-06-18)
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
const cursor = request.params?.cursor;
const config = getToolPaginationConfig();

// Get pagination info for logging
const paginationInfo = getPaginationInfo(TOOLS_DEFINITION.length, cursor, config);

console.error(`[Instantly MCP] 📋 Listing tools (page ${paginationInfo.currentPage}/${paginationInfo.totalPages}, ` +
`showing ${paginationInfo.startIndex + 1}-${paginationInfo.endIndex} of ${paginationInfo.totalTools})...`);

// Paginate tools for context window efficiency
const { tools, nextCursor } = paginateTools(TOOLS_DEFINITION, cursor, config);

if (nextCursor) {
console.error(`[Instantly MCP] 📄 More tools available - nextCursor provided for lazy loading`);
} else {
console.error(`[Instantly MCP] ✅ All tools returned (pagination ${config.enabled ? 'enabled' : 'disabled'})`);
}

return {
tools: TOOLS_DEFINITION
tools,
...(nextCursor && { nextCursor })
};
});

Expand Down
Loading