From 709cf6dc324062302ee6d89842e2163fd0f64c21 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Mon, 11 Aug 2025 15:09:23 -0500 Subject: [PATCH 1/2] chore: poc of multi-repo --- .vscode/mcp.json | 2 +- EXTERNAL_TOOL_PARAMETERS.md | 174 ++++++++++++++++++ src/shared/params.ts | 6 +- src/tools/EXTERNAL/external-tool-query-org.ts | 82 +++++++++ src/tools/data/index.ts | 2 +- src/tools/data/sf-query-org-using-external.ts | 67 +++++++ 6 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 EXTERNAL_TOOL_PARAMETERS.md create mode 100644 src/tools/EXTERNAL/external-tool-query-org.ts create mode 100644 src/tools/data/sf-query-org-using-external.ts diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 9f969be..ec92075 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -2,7 +2,7 @@ "servers": { "salesforce": { "command": "node", - "args": ["${workspaceFolder}/bin/run.js", "--toolsets", "all", "--orgs", "DEFAULT_TARGET_ORG", "--no-telemetry"] + "args": ["./bin/run.js", "--toolsets", "all", "--orgs", "ALLOW_ALL_ORGS", "--no-telemetry"] } } } diff --git a/EXTERNAL_TOOL_PARAMETERS.md b/EXTERNAL_TOOL_PARAMETERS.md new file mode 100644 index 0000000..4d51f92 --- /dev/null +++ b/EXTERNAL_TOOL_PARAMETERS.md @@ -0,0 +1,174 @@ +# External Tool Parameters Pattern + +This document explains how to handle parameters when creating external tools that integrate with the core MCP server. + +## Problem Statement + +When creating external tools that exist in separate NPM packages, we need to handle three types of parameters: + +1. **Core-only parameters**: Used only in the tool setup (e.g., `directory`, `usernameOrAlias`) +2. **External-only parameters**: Used only by the external tool (e.g., `query`) +3. **Shared parameters**: Used by both core setup and external tool (e.g., `useToolingApi`) + +The challenge is avoiding circular dependencies while allowing external tools to reference shared parameters defined in the core MCP server. + +## Solution: Parameter Schema Mapping Pattern + +Since external tools cannot import from the core MCP server (to avoid circular dependencies), we use a **Parameter Schema Mapping Pattern** where: + +1. External tools define their own parameter schemas +2. Core MCP server maps shared parameters to external tool schemas +3. Core MCP server handles parameter validation and processing + +### 1. External Tool Implementation + +In your external tool (e.g., `external-tool-query-org.ts`): + +```typescript +import { z } from 'zod'; + +// Define the parameter schema for this external tool +// Note: External tools should define their own parameter schemas to avoid circular dependencies +export const queryOrgParamsSchema = z.object({ + query: z.string().describe('SOQL query to run'), + useToolingApi: z + .boolean() + .optional() + .default(false) + .describe('Use Tooling API for the operation (default is false).'), +}); + +export type ExternalQueryOrgParams = z.infer; + +// Pure tool logic function +export const queryOrgExecutable = async ( + params: ExternalQueryOrgParams, + connection: Connection +): Promise => { + const { query, useToolingApi } = params; + // ... implementation +}; +``` + +### 2. Core MCP Server Tool Registration + +In your core tool registration (e.g., `sf-query-org-using-external.ts`): + +```typescript +import { directoryParam, usernameOrAliasParam, useToolingApiParam } from '../../shared/params.js'; +import { queryOrgParamsSchema } from '../EXTERNAL/external-tool-query-org.js'; + +// Combine core parameters (used only in setup) with external tool parameters +// Note: We use the core's shared parameters for validation, but map them to the external tool's schema +export const queryOrgParamsSchema = z.object({ + // Core parameters (used only in tool setup) + usernameOrAlias: usernameOrAliasParam, + directory: directoryParam, + // External tool parameters (passed through to external tool) + query: queryOrgParamsSchema.shape.query, + useToolingApi: useToolingApiParam, // Use core's shared parameter for validation +}); + +export const registerToolQueryOrg = (server: SfMcpServer): void => { + server.tool( + 'sf-query-org', + queryOrgDescription, + queryOrgParamsSchema.shape, + { + title: 'Query Org', + openWorldHint: false, + readOnlyHint: true, + }, + async ({ directory, usernameOrAlias, query, useToolingApi }) => { + process.chdir(directory); + const connection = await getConnection(usernameOrAlias); + + // Extract only the parameters that the external tool expects + const passthroughParams = { + query, + useToolingApi, + }; + + return queryOrgExecutable(passthroughParams, connection); + } + ); +}; +``` + +## Parameter Categories + +### 1. Core-only Parameters + +- **Purpose**: Used only in the tool setup/registration +- **Examples**: `directory`, `usernameOrAlias` +- **Handling**: Defined in core MCP server, not passed to external tool + +### 2. External-only Parameters + +- **Purpose**: Used only by the external tool logic +- **Examples**: `query` (for SOQL queries) +- **Handling**: Defined in external tool, passed through from core + +### 3. Shared Parameters + +- **Purpose**: Used by both core setup and external tool +- **Examples**: `useToolingApi` +- **Handling**: Defined in both core MCP server and external tool, mapped during registration + +## Benefits + +1. **No Circular Dependencies**: External tools don't import from core MCP server +2. **Type Safety**: Full TypeScript support with proper type inference +3. **Clean Separation**: Core handles setup, external tools handle business logic +4. **Reusability**: Shared parameters can be used across multiple external tools +5. **Maintainability**: Clear separation of concerns + +## Migration Guide + +To migrate existing external tools to this pattern: + +1. **Identify parameter types**: Categorize parameters as core-only, external-only, or shared +2. **Update external tool**: Define parameter schema locally (no imports from core) +3. **Update core registration**: Map shared parameters using core's shared parameter schemas +4. **Test thoroughly**: Ensure all parameters are properly passed through + +## Example: Adding a New Shared Parameter + +1. **Add to external tool**: + + ```typescript + export const queryOrgParamsSchema = z.object({ + query: z.string().describe('SOQL query to run'), + useToolingApi: z + .boolean() + .optional() + .default(false) + .describe('Use Tooling API for the operation (default is false).'), + newSharedParam: z.string().describe('New shared parameter'), + }); + ``` + +2. **Add to core shared params** (`src/shared/params.ts`): + + ```typescript + export const newSharedParam = z.string().describe('New shared parameter'); + ``` + +3. **Update core registration**: + ```typescript + export const queryOrgParamsSchema = z.object({ + usernameOrAlias: usernameOrAliasParam, + directory: directoryParam, + query: queryOrgParamsSchema.shape.query, + useToolingApi: useToolingApiParam, + newSharedParam: newSharedParam, // Use core's shared parameter + }); + ``` + +## Best Practices + +1. **Keep external tools independent**: Don't import from core MCP server +2. **Use descriptive parameter names**: Make it clear what each parameter does +3. **Maintain consistency**: Use similar parameter schemas across related tools +4. **Document parameter purposes**: Add clear descriptions for all parameters +5. **Test parameter mapping**: Ensure shared parameters are correctly passed through diff --git a/src/shared/params.ts b/src/shared/params.ts index e470a5d..5bd83f8 100644 --- a/src/shared/params.ts +++ b/src/shared/params.ts @@ -36,7 +36,11 @@ USAGE: ...for my 'test@example.com' user ...for the 'test@example.com' org`); -export const useToolingApiParam = z.boolean().optional().describe('Use Tooling API for the operation'); +export const useToolingApiParam = z + .boolean() + .optional() + .default(false) + .describe('Use Tooling API for the operation (default is false).'); export const baseAbsolutePathParam = z .string() diff --git a/src/tools/EXTERNAL/external-tool-query-org.ts b/src/tools/EXTERNAL/external-tool-query-org.ts new file mode 100644 index 0000000..dfc376a --- /dev/null +++ b/src/tools/EXTERNAL/external-tool-query-org.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * External tool logic for querying Salesforce orgs + * This file contains only the pure business logic that can be exported to external repos + */ + +import { type Connection } from '@salesforce/core'; +import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; + +// Define the parameter schema for this external tool +export const queryOrgParamsSchema = z.object({ + query: z.string().describe('SOQL query to run'), + useToolingApi: z + .boolean() + .optional() + .default(false) + .describe('Use Tooling API for the operation (default is false).'), +}); + +export type ExternalQueryOrgParams = z.infer; + +// Description of the external tool +export const queryOrgDescription = 'Run a SOQL query against a Salesforce org.'; + +// Annotations for the external tool +// https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations +export const queryOrgAnnotations = { + title: 'Query Org', + openWorldHint: false, + readOnlyHint: true, +}; + +// Logic-only function that can be imported in external MCP Servers +export const queryOrgExecutable = async ( + params: ExternalQueryOrgParams, + config: { + connection: Connection; + } +): Promise => { + const { query, useToolingApi } = params; + const { connection } = config; + + try { + const result = useToolingApi ? await connection.tooling.query(query) : await connection.query(query); + + return { + isError: false, + content: [ + { + type: 'text', + text: `SOQL query results:\n\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text', + text: `Failed to query org: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + ], + }; + } +}; diff --git a/src/tools/data/index.ts b/src/tools/data/index.ts index 0181b3b..380e1a3 100644 --- a/src/tools/data/index.ts +++ b/src/tools/data/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './sf-query-org.js'; +export * from './sf-query-org-using-external.js'; diff --git a/src/tools/data/sf-query-org-using-external.ts b/src/tools/data/sf-query-org-using-external.ts new file mode 100644 index 0000000..bdc28a0 --- /dev/null +++ b/src/tools/data/sf-query-org-using-external.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; + +import { getConnection } from '../../shared/auth.js'; +import { directoryParam, usernameOrAliasParam, useToolingApiParam } from '../../shared/params.js'; +import { SfMcpServer } from '../../sf-mcp-server.js'; +import { + queryOrgDescription, + queryOrgAnnotations, + queryOrgExecutable, + queryOrgParamsSchema as queryOrgParamsSchemaExternal, +} from '../EXTERNAL/external-tool-query-org.js'; + +export const queryOrgParamsSchema = z.object({ + // Shared parameters (used only in tool setup) + usernameOrAlias: usernameOrAliasParam, + directory: directoryParam, + // Shared parameters (passed through to external tool) + useToolingApi: useToolingApiParam, + // External tool parameters (passed through to external tool) + query: queryOrgParamsSchemaExternal.shape.query, +}); + +export type QueryOrgOptions = z.infer; + +export const registerToolQueryOrg = (server: SfMcpServer): void => { + server.tool( + 'sf-query-org', + `${queryOrgDescription} + + EXAMPLES: + ...query Contacts that have a Phone listed + ...find the 3 newest Property__c records`, + queryOrgParamsSchema.shape, + queryOrgAnnotations, + async ({ directory, usernameOrAlias, query, useToolingApi }) => { + process.chdir(directory); + const connection = await getConnection(usernameOrAlias); + + const passThroughParams = { + query, + useToolingApi, + }; + + const passThroughConfig = { + connection, + }; + + return queryOrgExecutable(passThroughParams, passThroughConfig); + } + ); +}; From fbdc1e83854dfc37a6872063963dc909d26eaa4d Mon Sep 17 00:00:00 2001 From: Willhoit Date: Tue, 12 Aug 2025 13:59:50 -0500 Subject: [PATCH 2/2] chore: temp remove md --- EXTERNAL_TOOL_PARAMETERS.md | 174 ------------------------------------ 1 file changed, 174 deletions(-) delete mode 100644 EXTERNAL_TOOL_PARAMETERS.md diff --git a/EXTERNAL_TOOL_PARAMETERS.md b/EXTERNAL_TOOL_PARAMETERS.md deleted file mode 100644 index 4d51f92..0000000 --- a/EXTERNAL_TOOL_PARAMETERS.md +++ /dev/null @@ -1,174 +0,0 @@ -# External Tool Parameters Pattern - -This document explains how to handle parameters when creating external tools that integrate with the core MCP server. - -## Problem Statement - -When creating external tools that exist in separate NPM packages, we need to handle three types of parameters: - -1. **Core-only parameters**: Used only in the tool setup (e.g., `directory`, `usernameOrAlias`) -2. **External-only parameters**: Used only by the external tool (e.g., `query`) -3. **Shared parameters**: Used by both core setup and external tool (e.g., `useToolingApi`) - -The challenge is avoiding circular dependencies while allowing external tools to reference shared parameters defined in the core MCP server. - -## Solution: Parameter Schema Mapping Pattern - -Since external tools cannot import from the core MCP server (to avoid circular dependencies), we use a **Parameter Schema Mapping Pattern** where: - -1. External tools define their own parameter schemas -2. Core MCP server maps shared parameters to external tool schemas -3. Core MCP server handles parameter validation and processing - -### 1. External Tool Implementation - -In your external tool (e.g., `external-tool-query-org.ts`): - -```typescript -import { z } from 'zod'; - -// Define the parameter schema for this external tool -// Note: External tools should define their own parameter schemas to avoid circular dependencies -export const queryOrgParamsSchema = z.object({ - query: z.string().describe('SOQL query to run'), - useToolingApi: z - .boolean() - .optional() - .default(false) - .describe('Use Tooling API for the operation (default is false).'), -}); - -export type ExternalQueryOrgParams = z.infer; - -// Pure tool logic function -export const queryOrgExecutable = async ( - params: ExternalQueryOrgParams, - connection: Connection -): Promise => { - const { query, useToolingApi } = params; - // ... implementation -}; -``` - -### 2. Core MCP Server Tool Registration - -In your core tool registration (e.g., `sf-query-org-using-external.ts`): - -```typescript -import { directoryParam, usernameOrAliasParam, useToolingApiParam } from '../../shared/params.js'; -import { queryOrgParamsSchema } from '../EXTERNAL/external-tool-query-org.js'; - -// Combine core parameters (used only in setup) with external tool parameters -// Note: We use the core's shared parameters for validation, but map them to the external tool's schema -export const queryOrgParamsSchema = z.object({ - // Core parameters (used only in tool setup) - usernameOrAlias: usernameOrAliasParam, - directory: directoryParam, - // External tool parameters (passed through to external tool) - query: queryOrgParamsSchema.shape.query, - useToolingApi: useToolingApiParam, // Use core's shared parameter for validation -}); - -export const registerToolQueryOrg = (server: SfMcpServer): void => { - server.tool( - 'sf-query-org', - queryOrgDescription, - queryOrgParamsSchema.shape, - { - title: 'Query Org', - openWorldHint: false, - readOnlyHint: true, - }, - async ({ directory, usernameOrAlias, query, useToolingApi }) => { - process.chdir(directory); - const connection = await getConnection(usernameOrAlias); - - // Extract only the parameters that the external tool expects - const passthroughParams = { - query, - useToolingApi, - }; - - return queryOrgExecutable(passthroughParams, connection); - } - ); -}; -``` - -## Parameter Categories - -### 1. Core-only Parameters - -- **Purpose**: Used only in the tool setup/registration -- **Examples**: `directory`, `usernameOrAlias` -- **Handling**: Defined in core MCP server, not passed to external tool - -### 2. External-only Parameters - -- **Purpose**: Used only by the external tool logic -- **Examples**: `query` (for SOQL queries) -- **Handling**: Defined in external tool, passed through from core - -### 3. Shared Parameters - -- **Purpose**: Used by both core setup and external tool -- **Examples**: `useToolingApi` -- **Handling**: Defined in both core MCP server and external tool, mapped during registration - -## Benefits - -1. **No Circular Dependencies**: External tools don't import from core MCP server -2. **Type Safety**: Full TypeScript support with proper type inference -3. **Clean Separation**: Core handles setup, external tools handle business logic -4. **Reusability**: Shared parameters can be used across multiple external tools -5. **Maintainability**: Clear separation of concerns - -## Migration Guide - -To migrate existing external tools to this pattern: - -1. **Identify parameter types**: Categorize parameters as core-only, external-only, or shared -2. **Update external tool**: Define parameter schema locally (no imports from core) -3. **Update core registration**: Map shared parameters using core's shared parameter schemas -4. **Test thoroughly**: Ensure all parameters are properly passed through - -## Example: Adding a New Shared Parameter - -1. **Add to external tool**: - - ```typescript - export const queryOrgParamsSchema = z.object({ - query: z.string().describe('SOQL query to run'), - useToolingApi: z - .boolean() - .optional() - .default(false) - .describe('Use Tooling API for the operation (default is false).'), - newSharedParam: z.string().describe('New shared parameter'), - }); - ``` - -2. **Add to core shared params** (`src/shared/params.ts`): - - ```typescript - export const newSharedParam = z.string().describe('New shared parameter'); - ``` - -3. **Update core registration**: - ```typescript - export const queryOrgParamsSchema = z.object({ - usernameOrAlias: usernameOrAliasParam, - directory: directoryParam, - query: queryOrgParamsSchema.shape.query, - useToolingApi: useToolingApiParam, - newSharedParam: newSharedParam, // Use core's shared parameter - }); - ``` - -## Best Practices - -1. **Keep external tools independent**: Don't import from core MCP server -2. **Use descriptive parameter names**: Make it clear what each parameter does -3. **Maintain consistency**: Use similar parameter schemas across related tools -4. **Document parameter purposes**: Add clear descriptions for all parameters -5. **Test parameter mapping**: Ensure shared parameters are correctly passed through