@withone/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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Utility Helper Functions
3
+ *
4
+ * This module contains reusable utility functions used throughout the Pica MCP server.
5
+ *
6
+ * @fileoverview Utility functions for data processing and API interactions
7
+ * @author Pica
8
+ */
9
+ import { PermissionLevel } from './types.js';
10
+ /**
11
+ * Paginates through API results by repeatedly calling a fetch function until all data is retrieved.
12
+ * @param fetchFn - Function that fetches paginated data, takes skip and limit parameters
13
+ * @param limit - Maximum number of items to fetch per request (default: 100)
14
+ * @returns Promise that resolves to an array of all paginated results
15
+ */
16
+ export declare function paginateResults<T>(fetchFn: (skip: number, limit: number) => Promise<{
17
+ rows: T[];
18
+ total: number;
19
+ skip: number;
20
+ limit: number;
21
+ }>, limit?: number): Promise<T[]>;
22
+ /**
23
+ * Generic function to fetch paginated data from any API endpoint
24
+ * @param baseUrl - The base URL for the API endpoint (without query parameters)
25
+ * @param headers - Headers to include in the request
26
+ * @param additionalParams - Optional additional parameters to include in the request
27
+ * @returns Promise that resolves to an array of all paginated results
28
+ */
29
+ export declare function fetchPaginatedData<T>(baseUrl: string, headers: Record<string, string>, additionalParams?: Record<string, string | number | boolean>): Promise<T[]>;
30
+ /**
31
+ * Builds action knowledge with API request structure guidance.
32
+ * @param knowledge - The raw knowledge content for the action
33
+ * @param method - The HTTP method for the action
34
+ * @param baseUrl - The base URL for Pica API
35
+ * @param platform - The platform name (used for connection key)
36
+ * @param actionId - The action ID
37
+ * @returns Complete formatted knowledge with API guidance
38
+ */
39
+ export declare function buildActionKnowledgeWithGuidance(knowledge: string, method: string, baseUrl: string, platform: string, actionId: string): string;
40
+ /**
41
+ * Replaces path variables in a template string with actual values.
42
+ * Path variables can be in either format: {variableName} or {{variableName}} and will be replaced with corresponding values.
43
+ * @param path - The template string containing path variables in {variableName} or {{variableName}} format
44
+ * @param variables - Object containing variable names as keys and their replacement values
45
+ * @returns The path string with all variables replaced by their encoded values
46
+ * @throws Error if any required variable is missing, null, undefined, or empty string
47
+ */
48
+ export declare function replacePathVariables(path: string, variables: Record<string, string | number | boolean>): string;
49
+ export declare function filterByPermissions<T extends {
50
+ method: string;
51
+ }>(actions: T[], permissions: PermissionLevel): T[];
52
+ export declare function isMethodAllowed(method: string, permissions: PermissionLevel): boolean;
53
+ export declare function isActionAllowed(actionId: string, allowedActionIds: string[]): boolean;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Utility Helper Functions
3
+ *
4
+ * This module contains reusable utility functions used throughout the One MCP server.
5
+ *
6
+ * @fileoverview Utility functions for data processing and API interactions
7
+ * @author One
8
+ */
9
+ import axios from 'axios';
10
+ /**
11
+ * Paginates through API results by repeatedly calling a fetch function until all data is retrieved.
12
+ * @param fetchFn - Function that fetches paginated data, takes skip and limit parameters
13
+ * @param limit - Maximum number of items to fetch per request (default: 100)
14
+ * @returns Promise that resolves to an array of all paginated results
15
+ */
16
+ export async function paginateResults(fetchFn, limit = 100) {
17
+ let page = 1;
18
+ let allResults = [];
19
+ let total = 0;
20
+ try {
21
+ do {
22
+ const response = await fetchFn(page, limit);
23
+ const { rows, total: totalCount } = response;
24
+ total = totalCount;
25
+ allResults = [...allResults, ...rows];
26
+ page++;
27
+ } while (allResults.length < total);
28
+ return allResults;
29
+ }
30
+ catch (error) {
31
+ console.error("Error in pagination:", error);
32
+ throw error;
33
+ }
34
+ }
35
+ /**
36
+ * Generic function to fetch paginated data from any API endpoint
37
+ * @param baseUrl - The base URL for the API endpoint (without query parameters)
38
+ * @param headers - Headers to include in the request
39
+ * @param additionalParams - Optional additional parameters to include in the request
40
+ * @returns Promise that resolves to an array of all paginated results
41
+ */
42
+ export async function fetchPaginatedData(baseUrl, headers, additionalParams) {
43
+ const fetchFn = async (page, limit) => {
44
+ const params = {
45
+ page,
46
+ limit,
47
+ ...additionalParams
48
+ };
49
+ try {
50
+ const response = await axios.get(baseUrl, {
51
+ headers,
52
+ params
53
+ });
54
+ return response.data;
55
+ }
56
+ catch (error) {
57
+ console.error(`Failed to fetch from ${baseUrl}:`, error);
58
+ throw error;
59
+ }
60
+ };
61
+ return paginateResults(fetchFn);
62
+ }
63
+ /**
64
+ * Builds action knowledge with API request structure guidance.
65
+ * @param knowledge - The raw knowledge content for the action
66
+ * @param method - The HTTP method for the action
67
+ * @param baseUrl - The base URL for One API
68
+ * @param platform - The platform name (used for connection key)
69
+ * @param actionId - The action ID
70
+ * @returns Complete formatted knowledge with API guidance
71
+ */
72
+ export function buildActionKnowledgeWithGuidance(knowledge, method, baseUrl, platform, actionId) {
73
+ const cleanBaseUrl = baseUrl.replace(/\/$/, '');
74
+ return `${knowledge}
75
+
76
+ API REQUEST STRUCTURE
77
+ ======================
78
+ URL: ${cleanBaseUrl}/v1/passthrough/{{PATH}}
79
+
80
+ IMPORTANT: When constructing the URL, only include the API endpoint path after the base URL.
81
+ Do NOT include the full third-party API URL.
82
+
83
+ Examples:
84
+ ✅ Correct: ${cleanBaseUrl}/v1/passthrough/crm/v3/objects/contacts/search
85
+ ❌ Incorrect: ${cleanBaseUrl}/v1/passthrough/https://api.hubapi.com/crm/v3/objects/contacts/search
86
+
87
+ METHOD: ${method}
88
+
89
+ HEADERS:
90
+ - x-one-secret: {{process.env.ONE_SECRET}}
91
+ - x-one-connection-key: {{process.env.ONE_${platform.toUpperCase()}_CONNECTION_KEY}}
92
+ - x-one-action-id: ${actionId}
93
+ - ... (other headers)
94
+
95
+ BODY: {{BODY}}
96
+
97
+ QUERY PARAMS: {{QUERY_PARAMS}}`;
98
+ }
99
+ /**
100
+ * Replaces path variables in a template string with actual values.
101
+ * Path variables can be in either format: {variableName} or {{variableName}} and will be replaced with corresponding values.
102
+ * @param path - The template string containing path variables in {variableName} or {{variableName}} format
103
+ * @param variables - Object containing variable names as keys and their replacement values
104
+ * @returns The path string with all variables replaced by their encoded values
105
+ * @throws Error if any required variable is missing, null, undefined, or empty string
106
+ */
107
+ export function replacePathVariables(path, variables) {
108
+ if (!path)
109
+ return path;
110
+ let result = path;
111
+ // First, replace double bracket variables {{variableName}}
112
+ result = result.replace(/\{\{([^}]+)\}\}/g, (match, variable) => {
113
+ const trimmedVariable = variable.trim();
114
+ const value = variables[trimmedVariable];
115
+ if (value === undefined || value === null || value === '') {
116
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
117
+ }
118
+ return encodeURIComponent(value.toString());
119
+ });
120
+ // Then, replace single bracket variables {variableName}
121
+ result = result.replace(/\{([^}]+)\}/g, (match, variable) => {
122
+ const trimmedVariable = variable.trim();
123
+ const value = variables[trimmedVariable];
124
+ if (value === undefined || value === null || value === '') {
125
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
126
+ }
127
+ return encodeURIComponent(value.toString());
128
+ });
129
+ return result;
130
+ }
131
+ const PERMISSION_METHODS = {
132
+ read: ["GET"],
133
+ write: ["GET", "POST", "PUT", "PATCH"],
134
+ admin: null,
135
+ };
136
+ export function filterByPermissions(actions, permissions) {
137
+ const allowed = PERMISSION_METHODS[permissions];
138
+ if (allowed === null)
139
+ return actions;
140
+ return actions.filter((a) => allowed.includes(a.method.toUpperCase()));
141
+ }
142
+ export function isMethodAllowed(method, permissions) {
143
+ const allowed = PERMISSION_METHODS[permissions];
144
+ if (allowed === null)
145
+ return true;
146
+ return allowed.includes(method.toUpperCase());
147
+ }
148
+ export function isActionAllowed(actionId, allowedActionIds) {
149
+ return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
150
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pica MCP Server - Main Entry Point
4
+ *
5
+ * This is the main MCP (Model Context Protocol) server implementation for Pica.
6
+ *
7
+ * @fileoverview Main MCP server implementation with Pica API integration
8
+ * @author Pica
9
+ */
10
+ export { createPicaServer, type CreatePicaServerOptions } from './server.js';
package/build/index.js ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One MCP Server - Main Entry Point
4
+ *
5
+ * This is the main MCP (Model Context Protocol) server implementation for One.
6
+ *
7
+ * @fileoverview Main MCP server implementation with One API integration
8
+ * @author One
9
+ */
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
13
+ import { OneClient } from './client.js';
14
+ import { buildActionKnowledgeWithGuidance, filterByPermissions, isMethodAllowed, isActionAllowed, } from './helpers.js';
15
+ import { listOneIntegrationsToolConfig, searchOnePlatformActionsToolConfig, getOneActionKnowledgeToolConfig, executeOneActionToolConfig } from './schemas.js';
16
+ const SERVER_NAME = "one-mcp-server";
17
+ const SERVER_VERSION = "1.0.0";
18
+ const ONE_SECRET = process.env.ONE_SECRET;
19
+ if (!ONE_SECRET) {
20
+ console.error("ONE_SECRET environment variable is required");
21
+ process.exit(1);
22
+ }
23
+ const ONE_BASE_URL = process.env.ONE_BASE_URL || "https://api.withone.ai";
24
+ const ONE_IDENTITY = process.env.ONE_IDENTITY;
25
+ const ONE_IDENTITY_TYPE = process.env.ONE_IDENTITY_TYPE;
26
+ const ONE_PERMISSIONS = process.env.ONE_PERMISSIONS || "admin";
27
+ if (!["read", "write", "admin"].includes(ONE_PERMISSIONS)) {
28
+ console.error(`Invalid ONE_PERMISSIONS value: "${ONE_PERMISSIONS}". Must be one of: read, write, admin`);
29
+ process.exit(1);
30
+ }
31
+ const ONE_CONNECTION_KEYS = process.env.ONE_CONNECTION_KEYS
32
+ ? process.env.ONE_CONNECTION_KEYS.split(",").map(s => s.trim()).filter(Boolean)
33
+ : ["*"];
34
+ const ONE_ACTION_IDS = process.env.ONE_ACTION_IDS
35
+ ? process.env.ONE_ACTION_IDS.split(",").map(s => s.trim()).filter(Boolean)
36
+ : ["*"];
37
+ const ONE_KNOWLEDGE_AGENT = process.env.ONE_KNOWLEDGE_AGENT === "true";
38
+ const oneClient = new OneClient({
39
+ secret: ONE_SECRET,
40
+ baseUrl: ONE_BASE_URL,
41
+ identity: ONE_IDENTITY,
42
+ identityType: ONE_IDENTITY_TYPE,
43
+ connectionKeys: ONE_CONNECTION_KEYS,
44
+ });
45
+ let oneInitialized = false;
46
+ let initializationPromise = null;
47
+ const initializeOne = async () => {
48
+ if (oneInitialized) {
49
+ return;
50
+ }
51
+ if (initializationPromise) {
52
+ return initializationPromise;
53
+ }
54
+ initializationPromise = (async () => {
55
+ try {
56
+ await oneClient.initialize();
57
+ oneInitialized = true;
58
+ }
59
+ catch (error) {
60
+ console.error("Failed to initialize One client:", error);
61
+ initializationPromise = null;
62
+ throw error;
63
+ }
64
+ })();
65
+ return initializationPromise;
66
+ };
67
+ const server = new McpServer({
68
+ name: SERVER_NAME,
69
+ version: SERVER_VERSION,
70
+ });
71
+ server.registerTool("list_one_integrations", listOneIntegrationsToolConfig, async (args) => {
72
+ await initializeOne();
73
+ return await handleGetIntegrations(args);
74
+ });
75
+ server.registerTool("search_one_platform_actions", searchOnePlatformActionsToolConfig, async (args) => {
76
+ await initializeOne();
77
+ return await handleSearchPlatformActions(args);
78
+ });
79
+ server.registerTool("get_one_action_knowledge", getOneActionKnowledgeToolConfig, async (args) => {
80
+ await initializeOne();
81
+ return await handleGetActionKnowledge(args);
82
+ });
83
+ if (!ONE_KNOWLEDGE_AGENT) {
84
+ server.registerTool("execute_one_action", executeOneActionToolConfig, async (args) => {
85
+ await initializeOne();
86
+ return await handleExecuteOneAction(args);
87
+ });
88
+ }
89
+ async function handleGetIntegrations(args) {
90
+ try {
91
+ const connectedIntegrations = oneClient.getUserConnections();
92
+ const availableIntegrations = oneClient.getAvailableConnectors();
93
+ const activeConnections = connectedIntegrations.filter(conn => conn.active);
94
+ let activePlatforms = availableIntegrations.filter(def => def.active && !def.deprecated);
95
+ // When connection keys are scoped, only show platforms that match active connections
96
+ if (!ONE_CONNECTION_KEYS.includes("*")) {
97
+ const connectedPlatforms = new Set(activeConnections.map(conn => conn.platform));
98
+ activePlatforms = activePlatforms.filter(def => connectedPlatforms.has(def.platform));
99
+ }
100
+ const structuredResponse = {
101
+ connections: activeConnections.map(conn => ({
102
+ platform: conn.platform,
103
+ key: conn.key,
104
+ tags: conn.tags ?? []
105
+ })),
106
+ availablePlatforms: activePlatforms.map(def => ({
107
+ platform: def.platform,
108
+ name: def.name,
109
+ category: def.category
110
+ })),
111
+ summary: {
112
+ connectedCount: activeConnections.length,
113
+ availableCount: activePlatforms.length
114
+ }
115
+ };
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: JSON.stringify(structuredResponse, null, 2),
121
+ },
122
+ ],
123
+ structuredContent: structuredResponse,
124
+ };
125
+ }
126
+ catch (error) {
127
+ throw new McpError(ErrorCode.InternalError, `Failed to retrieve integrations: ${error instanceof Error ? error.message : 'Unknown error'}`);
128
+ }
129
+ }
130
+ async function handleSearchPlatformActions(args) {
131
+ try {
132
+ // Force knowledge mode when ONE_KNOWLEDGE_AGENT is enabled
133
+ const agentType = ONE_KNOWLEDGE_AGENT ? "knowledge" : args.agentType;
134
+ let actions = await oneClient.searchAvailableActions(args.platform, args.query, agentType);
135
+ // Apply permission-level filtering
136
+ actions = filterByPermissions(actions, ONE_PERMISSIONS);
137
+ // Apply action allowlist filtering
138
+ actions = actions.filter(a => isActionAllowed(a.systemId, ONE_ACTION_IDS));
139
+ const cleanedActions = actions.map(action => ({
140
+ actionId: action.systemId,
141
+ title: action.title,
142
+ method: action.method,
143
+ path: action.path
144
+ }));
145
+ // Handle empty results with helpful suggestions
146
+ if (cleanedActions.length === 0) {
147
+ const suggestionsText = `No actions found for platform '${args.platform}' matching query '${args.query}'.
148
+
149
+ SUGGESTIONS:
150
+ - Try a more general query (e.g., 'list', 'get', 'search', 'create')
151
+ - Verify the platform name is correct
152
+ - Check that actions exist for this platform using list_one_integrations
153
+
154
+ EXAMPLES OF GOOD QUERIES:
155
+ - "search contacts"
156
+ - "send email"
157
+ - "create customer"
158
+ - "list orders"`;
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: suggestionsText,
164
+ },
165
+ ],
166
+ };
167
+ }
168
+ // Build structured response
169
+ const structuredResponse = {
170
+ actions: cleanedActions,
171
+ metadata: {
172
+ platform: args.platform,
173
+ query: args.query,
174
+ count: cleanedActions.length
175
+ }
176
+ };
177
+ const responseText = `Found ${cleanedActions.length} action(s) for platform '${args.platform}' matching query '${args.query}':
178
+
179
+ ${JSON.stringify(structuredResponse, null, 2)}
180
+
181
+ NEXT STEP: Use get_one_action_knowledge with an actionId to get detailed documentation before building requests or executing actions.`;
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: responseText,
187
+ },
188
+ ],
189
+ structuredContent: structuredResponse,
190
+ };
191
+ }
192
+ catch (error) {
193
+ throw new McpError(ErrorCode.InternalError, `Failed to search platform actions: ${error instanceof Error ? error.message : 'Unknown error'}`);
194
+ }
195
+ }
196
+ async function handleGetActionKnowledge(args) {
197
+ try {
198
+ const actionId = args.actionId;
199
+ if (!isActionAllowed(actionId, ONE_ACTION_IDS)) {
200
+ throw new McpError(ErrorCode.InvalidRequest, `Action "${actionId}" is not in the allowed action list`);
201
+ }
202
+ if (!ONE_CONNECTION_KEYS.includes("*")) {
203
+ const connectedPlatforms = oneClient.getUserConnections().map(c => c.platform);
204
+ if (!connectedPlatforms.includes(args.platform)) {
205
+ throw new McpError(ErrorCode.InvalidRequest, `Platform "${args.platform}" has no allowed connections`);
206
+ }
207
+ }
208
+ const { knowledge, method } = await oneClient.getActionKnowledge(actionId);
209
+ const knowledgeWithGuidance = buildActionKnowledgeWithGuidance(knowledge, method, oneClient.getBaseUrl(), args.platform, actionId);
210
+ return {
211
+ content: [
212
+ {
213
+ type: "text",
214
+ text: knowledgeWithGuidance,
215
+ },
216
+ ],
217
+ };
218
+ }
219
+ catch (error) {
220
+ throw new McpError(ErrorCode.InternalError, `Failed to retrieve action knowledge: ${error instanceof Error ? error.message : 'Unknown error'}`);
221
+ }
222
+ }
223
+ async function handleExecuteOneAction(args) {
224
+ try {
225
+ if (!isActionAllowed(args.actionId, ONE_ACTION_IDS)) {
226
+ throw new McpError(ErrorCode.InvalidRequest, `Action "${args.actionId}" is not in the allowed action list`);
227
+ }
228
+ if (!ONE_CONNECTION_KEYS.includes("*")) {
229
+ const allowedKeys = oneClient.getUserConnections().map(c => c.key);
230
+ if (!allowedKeys.includes(args.connectionKey)) {
231
+ throw new McpError(ErrorCode.InvalidRequest, `Connection key "${args.connectionKey}" is not allowed`);
232
+ }
233
+ }
234
+ const actionDetails = await oneClient.getActionDetails(args.actionId);
235
+ if (!isMethodAllowed(actionDetails.method, ONE_PERMISSIONS)) {
236
+ throw new McpError(ErrorCode.InvalidRequest, `Method "${actionDetails.method}" is not allowed under "${ONE_PERMISSIONS}" permission level`);
237
+ }
238
+ const result = await oneClient.executePassthroughRequest(args, actionDetails);
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: JSON.stringify(result, null, 2),
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ catch (error) {
249
+ throw new McpError(ErrorCode.InternalError, `Failed to execute One action: ${error instanceof Error ? error.message : 'Unknown error'}`);
250
+ }
251
+ }
252
+ async function main() {
253
+ try {
254
+ const transport = new StdioServerTransport();
255
+ await server.connect(transport);
256
+ }
257
+ catch (error) {
258
+ console.error("Failed to start server:", error);
259
+ process.exit(1);
260
+ }
261
+ }
262
+ process.on('SIGINT', async () => {
263
+ console.error('Shutting down server...');
264
+ process.exit(0);
265
+ });
266
+ process.on('SIGTERM', async () => {
267
+ console.error('Shutting down server...');
268
+ process.exit(0);
269
+ });
270
+ process.on('uncaughtException', (error) => {
271
+ console.error('Uncaught exception:', error);
272
+ process.exit(1);
273
+ });
274
+ process.on('unhandledRejection', (reason, promise) => {
275
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
276
+ process.exit(1);
277
+ });
278
+ main().catch((error) => {
279
+ console.error('Server error:', error);
280
+ process.exit(1);
281
+ });