# Directory Structure
```
├── .github
│ └── workflows
│ └── npm-publish.yml
├── .gitignore
├── .idea
│ ├── AugmentWebviewStateStore.xml
│ ├── codeStyles
│ │ ├── codeStyleConfig.xml
│ │ └── Project.xml
│ ├── git_toolbox_blame.xml
│ ├── git_toolbox_prj.xml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── jsLinters
│ │ └── eslint.xml
│ ├── modules.xml
│ ├── prettier.xml
│ ├── sf-mcp.iml
│ ├── shelf
│ │ ├── Uncommitted_changes_before_Checkout_at_4_8_25__22_47__Changes_.xml
│ │ └── Uncommitted_changes_before_Checkout_at_4_8_25,_22_47_[Changes]
│ │ └── shelved.patch
│ ├── vcs.xml
│ └── workspace.xml
├── .prettierrc
├── build
│ ├── index.js
│ ├── resources.js
│ ├── sfCommands.js
│ └── utils.js
├── CHANGELOG.md
├── CLAUDE.md
├── eslint.config.js
├── package-lock.json
├── package.json
├── README.md
├── run.sh
├── src
│ ├── index.ts
│ ├── resources.ts
│ ├── sfCommands.ts
│ └── utils.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"endOfLine":"lf"
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Salesforce CLI MCP Server
Model Context Protocol (MCP) server for providing Salesforce CLI functionality to LLM tools like Claude Desktop.
## Overview
This MCP server wraps the Salesforce CLI (`sf`) command-line tool and exposes its commands as MCP tools and resources, allowing LLM-powered agents to:
- View help information about Salesforce CLI topics and commands
- Execute Salesforce CLI commands with appropriate parameters
- Leverage Salesforce CLI capabilities in AI workflows
## Requirements
- Node.js 18+ and npm
- Salesforce CLI (`sf`) installed and configured
- Your Salesforce org credentials configured in the CLI
## Installation
```bash
# Clone the repository
git clone <repository-url>
cd sfMcp
# Install dependencies
npm install
```
## Usage
### Starting the server
```bash
# Basic usage
npm start
# With project roots
npm start /path/to/project1 /path/to/project2
# or using the convenience script
npm run with-roots /path/to/project1 /path/to/project2
# As an npx package with roots
npx -y codefriar/sf-mcp /path/to/project1 /path/to/project2
```
The MCP server uses stdio transport, which can be used with MCP clients
like the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or Claude Desktop.
### Configuring in Claude Desktop
To configure this MCP in Claude Desktop's `.claude.json` configuration:
```json
{
"tools": {
"salesforce": {
"command": "/path/to/node",
"args": [
"/path/to/sf-mcp/build/index.js",
"/path/to/project1",
"/path/to/project2"
]
}
}
}
```
Using the npm package directly:
```json
{
"tools": {
"salesforce": {
"command": "/path/to/npx",
"args": [
"-y",
"codefriar/sf-mcp",
"/path/to/project1",
"/path/to/project2"
]
}
}
}
```
### Development
```bash
# Watch mode (recompiles on file changes)
npm run dev
# In another terminal
npm start [optional project roots...]
```
## Available Tools and Resources
This MCP server provides Salesforce CLI commands as MCP tools. It automatically discovers and registers all available commands from the Salesforce CLI, and also specifically implements the most commonly used commands.
### Core Tools
- `sf_version` - Get the Salesforce CLI version information
- `sf_help` - Get help information for Salesforce CLI commands
- `sf_cache_clear` - Clear the command discovery cache
- `sf_cache_refresh` - Refresh the command discovery cache
### Project Directory Management (Roots)
For commands that require a Salesforce project context (like deployments), you must specify the project directory.
The MCP supports multiple project directories (roots) similar to the filesystem MCP.
#### Configuration Methods
**Method 1: Via Command Line Arguments**
```bash
# Start the MCP with project roots
npm start /path/to/project1 /path/to/project2
# or
npx -y codefriar/sf-mcp /path/to/project1 /path/to/project2
```
When configured this way, the roots will be automatically named `root1`, `root2`, etc.,
with the first one set as default.
**Method 2: Using MCP Tools**
- `sf_set_project_directory` - Set a Salesforce project directory to use for commands
- Parameters:
- `directory` - Path to a directory containing a sfdx-project.json file
- `name` - (Optional) Name for this project root
- `description` - (Optional) Description for this project root
- `isDefault` - (Optional) Set this root as the default for command execution
- `sf_list_roots` - List all configured project roots
- `sf_detect_project_directory` - Attempt to detect project directory from user messages
Example usage:
```
# Set project directory with a name
sf_set_project_directory --directory=/path/to/your/sfdx/project --name=project1 --isDefault=true
# List all configured roots
sf_list_roots
# Or include in your message:
"Please deploy the apex code from the project in /path/to/your/sfdx/project to my scratch org"
```
**Method 3: Claude Desktop Configuration**
Configure project roots in `.claude.json` as described below.
#### Using Project Roots
You can execute commands in specific project roots:
```
# Using resource URI
sf://roots/project1/commands/project deploy start --sourcedir=force-app
# Using rootName parameter
sf_project_deploy_start --sourcedir=force-app --rootName=project1
```
Project directory must be specified for commands such as deployments,
source retrieval, and other project-specific operations.
If multiple roots are configured, the default root will be used unless otherwise specified.
### Key Implemented Tools
The following commands are specifically implemented and guaranteed to work:
#### Organization Management
- `sf_org_list` - List Salesforce orgs
- Parameters: `json`, `verbose`
- `sf_auth_list_orgs` - List authenticated Salesforce orgs
- Parameters: `json`, `verbose`
- `sf_org_display` - Display details about an org
- Parameters: `targetusername`, `json`
- `sf_org_open` - Open an org in the browser
- Parameters: `targetusername`, `path`, `urlonly`
#### Apex Code
- `sf_apex_run` - Run anonymous Apex code
- Parameters: `targetusername`, `file`, `apexcode`, `json`
- `sf_apex_test_run` - Run Apex tests
- Parameters: `targetusername`, `testnames`, `suitenames`, `classnames`, `json`
#### Data Management
- `sf_data_query` - Execute a SOQL query
- Parameters: `targetusername`, `query`, `json`
- `sf_schema_list_objects` - List sObjects in the org
- Parameters: `targetusername`, `json`
- `sf_schema_describe` - Describe a Salesforce object
- Parameters: `targetusername`, `sobject`, `json`
#### Deployment
- `sf_project_deploy_start` - Deploy the source to an org
- Parameters: `targetusername`, `sourcedir`, `json`, `wait`
### Dynamically Discovered Tools
The server discovers all available Salesforce CLI commands and registers them as tools with format: `sf_<topic>_<command>`.
For example:
- `sf_apex_run` - Run anonymous Apex code
- `sf_data_query` - Execute a SOQL query
For nested topic commands, the tool name includes the full path with underscores:
- `sf_apex_log_get` - Get apex logs
- `sf_org_login_web` - Login to an org using web flow
The server also creates simplified aliases for common nested commands where possible:
- `sf_get` as an alias for `sf_apex_log_get`
- `sf_web` as an alias for `sf_org_login_web`
The available commands vary depending on the installed Salesforce CLI plugins.
> **Note:** Command discovery is cached to improve startup performance. If you install new SF CLI plugins, use the `sf_cache_refresh` tool to update the cache, then restart the server.
### Resources
The following resources provide documentation about Salesforce CLI:
- `sf://help` - Main CLI documentation
- `sf://topics/{topic}/help` - Topic help documentation
- `sf://commands/{command}/help` - Command help documentation
- `sf://topics/{topic}/commands/{command}/help` - Topic-command help documentation
- `sf://version` - Version information
- `sf://roots` - List all configured project roots
- `sf://roots/{root}/commands/{command}` - Execute a command in a specific project root
## How It Works
1. At startup, the server checks for a cached list of commands (stored in `~/.sf-mcp/command-cache.json`)
2. If a valid cache exists, it's used to register commands; otherwise, commands are discovered dynamically
3. During discovery, the server queries `sf commands --json` to get a complete list of available commands
4. Command metadata (including parameters and descriptions) is extracted directly from the JSON output
5. All commands are registered as MCP tools with appropriate parameter schemas
6. Resources are registered for help documentation
7. When a tool is called, the corresponding Salesforce CLI command is executed
### Project Roots Management
For commands that require a Salesforce project context:
1. The server checks if any project roots have been configured via `sf_set_project_directory`
2. If multiple roots are configured, it uses the default root unless a specific root is specified
3. If no roots are set, the server will prompt the user to specify a project directory
4. Commands are executed within the appropriate project directory, ensuring proper context
5. The user can add or switch between multiple project roots as needed
Project-specific commands (like deployments, retrievals, etc.)
will automatically run in the appropriate project directory.
For commands that don't require a project context, the working directory doesn't matter.
You can execute commands in specific project roots by:
- Using the resource URI: `sf://roots/{rootName}/commands/{command}`
- Providing a `rootName` parameter to command tools (internal implementation details)
- Setting a specific root as the default with `sf_set_project_directory --isDefault=true`
### Command Caching
To improve startup performance, the MCP server caches discovered commands:
- The cache is stored in `~/.sf-mcp/command-cache.json`
- It includes all topics, commands, parameters, and descriptions
- The cache has a validation timestamp and SF CLI version check
- By default, the cache expires after 7 days
- When you install new Salesforce CLI plugins, use `sf_cache_refresh` to update the cache
#### Troubleshooting Cache Issues
The first run of the server performs a full command discovery which can take some time. If you encounter any issues with missing commands or cache problems:
1. Stop the MCP server (if running)
2. Manually delete the cache file: `rm ~/.sf-mcp/command-cache.json`
3. Start the server again: `npm start`
This will force a complete rediscovery of all commands using the official CLI metadata.
If specific commands are still missing, or you've installed new SF CLI plugins:
1. Use the `sf_cache_refresh` tool from Claude Desktop
2. Stop and restart the MCP server
### Handling Nested Topics
The Salesforce CLI has a hierarchical command structure that can be several levels deep. This MCP server handles these nested commands by:
- Converting colon-separated paths to underscore format (`apex:log:get` → `sf_apex_log_get`)
- Providing aliases for common deep commands when possible (`sf_get` for `sf_apex_log_get`)
- Preserving the full command hierarchy in the tool names
- Using the official command structure from `sf commands --json`
Nested topic commands are registered twice when possible—once with the full hierarchy name and once with a simplified alias,
making them easier to discover and use.
## License
ISC
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
# MCP Development Guide
## Build Commands
- Build project: `npm run build`
- Run the MCP server: `node build/index.js`
## Lint & Formatting
- Format with Prettier: `npx prettier --write 'src/**/*.ts'`
- Lint: `npx eslint 'src/**/*.ts'`
- Type check: `npx tsc --noEmit`
## Testing
- Run tests: `npm test`
- Run a single test: `npm test -- -t 'test name'`
## Code Style Guidelines
- Use ES modules (import/export) syntax
- TypeScript strict mode enabled
- Types: Use strong typing with TypeScript interfaces/types
- Naming: camelCase for variables/functions, PascalCase for classes/interfaces
- Error handling: Use try/catch with typed errors
- Imports: Group by 3rd party, then local, alphabetized within groups
- Async: Prefer async/await over raw Promises
- Documentation: JSDoc for public APIs
- Endpoint API naming following MCP conventions for resources/tools/prompts
## Project Description
- This project seeks to create a Model Context Protocol Server that tools like Claude code, and Claude desktop can use to directly and intelligently interface with the Salesforce Command Line Interface. (CLI)
## Model Context Protocol (MCP) Architecture
### Core Components
- **Hosts**: LLM applications (Claude Desktop, Claude Code) that initiate connections
- **Clients**: Maintain connections with MCP servers
- **Servers**: Provide context, tools, and prompts (this Salesforce CLI MCP server)
### MCP Primitives for Salesforce Integration
#### Tools
- Executable functions for Salesforce operations
- Dynamic tool discovery and invocation
- Tool annotations (read-only, destructive operations)
- Key Salesforce tools to implement:
- SOQL query execution
- Record CRUD operations
- Metadata deployment/retrieval
- Org inspection and configuration
- Apex execution and testing
#### Resources
- Expose Salesforce data and metadata
- Unique URI identification for resources
- Support for text and binary content
- Salesforce resources to expose:
- Object schemas and field definitions
- Org configuration and limits
- Deployment metadata
- Code coverage reports
- Flow definitions
#### Prompts
- Reusable prompt templates for Salesforce workflows
- Dynamic arguments for context-aware interactions
- Common Salesforce prompt patterns:
- Data analysis and reporting
- Code generation and review
- Deployment guidance
- Best practices recommendations
#### Sampling
- Allow server to request LLM completions
- Human-in-the-loop approval for destructive operations
- Fine-grained control over Salesforce operations
### Security Considerations
- Input validation for all Salesforce CLI commands
- Proper authentication with Salesforce orgs
- Rate limiting to respect Salesforce API limits
- Sanitization of external interactions
- Secure handling of sensitive org data
### Transport
- Primary: Stdio (standard input/output)
- Alternative: HTTP with Server-Sent Events (SSE)
### Implementation Strategy
1. Start with core Salesforce CLI tools (query, describe, deploy)
2. Use TypeScript MCP SDK for type safety
3. Implement robust error handling for CLI failures
4. Provide clear tool descriptions and examples
5. Add progressive enhancement for advanced features
```
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
```
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
```
--------------------------------------------------------------------------------
/.idea/git_toolbox_blame.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>
```
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>
```
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
```
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
```
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sf-mcp.iml" filepath="$PROJECT_DIR$/.idea/sf-mcp.iml" />
</modules>
</component>
</project>
```
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<work-dir-patterns value="$PROJECT_DIR$" />
<custom-configuration-file used="true" path="$PROJECT_DIR$/eslint.config.js" />
<option name="fix-on-save" value="true" />
</component>
</project>
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/.idea/shelf/Uncommitted_changes_before_Checkout_at_4_8_25__22_47__Changes_.xml:
--------------------------------------------------------------------------------
```
<changelist name="Uncommitted_changes_before_Checkout_at_4_8_25,_22_47_[Changes]" date="1744177621660" recycled="true" deleted="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/Uncommitted_changes_before_Checkout_at_4_8_25,_22_47_[Changes]/shelved.patch" />
<option name="DESCRIPTION" value="Uncommitted changes before Checkout at 4/8/25, 22:47 [Changes]" />
</changelist>
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
// tseslint.configs.strictTypeChecked,
// tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
);
```
--------------------------------------------------------------------------------
/.idea/git_toolbox_prj.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>
```
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
```
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Check if sf command is available
if ! command -v sf &> /dev/null; then
echo "Error: Salesforce CLI (sf) is not installed or not in your PATH"
echo "Please install it from: https://developer.salesforce.com/tools/sfdxcli"
exit 1
fi
# Print current directory and sf version
echo "Current directory: $(pwd)"
echo "Salesforce CLI version:"
sf --version
# Build and run the server
echo "Building and starting MCP server..."
npm run build
# Pass all command-line arguments to the server (for project roots)
node build/index.js "$@"
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "sf-mcp",
"version": "1.3.2",
"main": "build/index.js",
"type": "module",
"bin": {
"sfmcp": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js",
"start": "node build/index.js",
"dev": "tsc -w",
"lint": "eslint src",
"prepare": "npm run build",
"format": "prettier --write \"**/*.{ts,json,md}\"",
"test": "echo \"No tests configured\" && exit 0",
"release": "standard-version && git push --follow-tags origin main && npm publish",
"with-roots": "./run.sh"
},
"files": [
"build",
"run.sh"
],
"keywords": [
"mcp",
"modelcontextprotocol",
"salesforce",
"sf",
"cli",
"llm"
],
"author": "Kevin Poorman",
"license": "ISC",
"description": "Model Context Protocol (MCP) server for the Salesforce CLI, making Salesforce CLI commands available to LLM tools like Claude Desktop.",
"repository": {
"type": "git",
"url": "https://github.com/codefriar/sf-mcp"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
"@types/node": "^22.13.14",
"eslint": "^9.23.0",
"prettier": "^3.5.3",
"standard-release": "^0.2.0",
"standard-version": "^9.5.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0"
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.3.2](https://github.com/codefriar/sf-mcp/compare/v1.3.1...v1.3.2) (2025-05-27)
### [1.3.1](https://github.com/codefriar/sf-mcp/compare/v1.3.0...v1.3.1) (2025-04-09)
## [1.3.0](https://github.com/codefriar/sf-mcp/compare/v1.1.1...v1.3.0) (2025-04-09)
### Features
* add contextual execution with project directory detection ([0c02e01](https://github.com/codefriar/sf-mcp/commit/0c02e0100da6906ea0ece9e26e0fd75ec0886044))
* add Salesforce project directory handling for contextual command execution ([5c1ddc3](https://github.com/codefriar/sf-mcp/commit/5c1ddc3783a0e8e80f357dfb0c9c082e8710b36d))
* **context directories:** All commands require a directory ([4b1d76b](https://github.com/codefriar/sf-mcp/commit/4b1d76b0b38c9b5b01b12efbed2ad107320af3c2))
* **roots:** Now with Roots ([da38db3](https://github.com/codefriar/sf-mcp/commit/da38db3187809b42c47604d9d078238d2d02705a))
## [1.2.0](https://github.com/codefriar/sf-mcp/compare/v1.1.1...v1.2.0) (2025-04-09)
### Features
* add contextual execution with project directory detection ([0c02e01](https://github.com/codefriar/sf-mcp/commit/0c02e0100da6906ea0ece9e26e0fd75ec0886044))
* add Salesforce project directory handling for contextual command execution ([5c1ddc3](https://github.com/codefriar/sf-mcp/commit/5c1ddc3783a0e8e80f357dfb0c9c082e8710b36d))
* **context directories:** All commands require a directory ([4b1d76b](https://github.com/codefriar/sf-mcp/commit/4b1d76b0b38c9b5b01b12efbed2ad107320af3c2))
* **roots:** Now with Roots ([da38db3](https://github.com/codefriar/sf-mcp/commit/da38db3187809b42c47604d9d078238d2d02705a))
### [1.1.1](https://github.com/codefriar/sf-mcp/compare/v1.1.0...v1.1.1) (2025-04-02)
## 1.1.0 (2025-03-28)
### Features
* **tools:** autodiscovery of tools ([532c685](https://github.com/codefriar/sf-mcp/commit/532c685aa8b22f01e81b4bfa69024c14a05d932d))
```
--------------------------------------------------------------------------------
/src/resources.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { executeSfCommand, getProjectRoots } from './sfCommands.js';
/**
* Register all resources for the SF CLI MCP Server
*/
export function registerResources(server: McpServer): void {
// Main CLI documentation
server.resource('sf-help', 'sf://help', async (uri) => ({
contents: [
{
uri: uri.href,
text: executeSfCommand('-h'),
},
],
}));
// Project roots information
server.resource('sf-roots', 'sf://roots', async (uri) => {
const roots = getProjectRoots();
const rootsText = roots.length > 0
? roots.map(root => `${root.name}${root.isDefault ? ' (default)' : ''}: ${root.path}${root.description ? ` - ${root.description}` : ''}`).join('\n')
: 'No project roots configured. Use sf_set_project_directory to add a project root.';
return {
contents: [
{
uri: uri.href,
text: rootsText,
},
],
};
});
// Topic help documentation
server.resource(
'sf-topic-help',
new ResourceTemplate('sf://topics/{topic}/help', { list: undefined }),
async (uri, { topic }) => ({
contents: [
{
uri: uri.href,
text: executeSfCommand(`${topic} -h`),
},
],
})
);
// Command help documentation
server.resource(
'sf-command-help',
new ResourceTemplate('sf://commands/{command}/help', { list: undefined }),
async (uri, { command }) => ({
contents: [
{
uri: uri.href,
text: executeSfCommand(`${command} -h`),
},
],
})
);
// Topic-command help documentation
server.resource(
'sf-topic-command-help',
new ResourceTemplate('sf://topics/{topic}/commands/{command}/help', {
list: undefined,
}),
async (uri, { topic, command }) => ({
contents: [
{
uri: uri.href,
text: executeSfCommand(`${topic} ${command} -h`),
},
],
})
);
// Root-specific command help (execute in a specific root)
server.resource(
'sf-root-command',
new ResourceTemplate('sf://roots/{root}/commands/{command}', { list: undefined }),
async (uri, { root, command }) => ({
contents: [
{
uri: uri.href,
// Ensure command is treated as string
text: executeSfCommand(String(command), String(root)),
},
],
})
);
// Version information
server.resource('sf-version', 'sf://version', async (uri) => ({
contents: [
{
uri: uri.href,
text: executeSfCommand('--version'),
},
],
}));
}
```
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
```
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value=" " />
<ApexCodeStyleSettings>
<option name="ALIGN_MULTILINE_COMPOSITE_EXPRESSIONS" value="true" />
<option name="USAGE_CASE_APEX_CHANGE_CASE_WHEN_FORMATTING" value="true" />
<option name="KEYWORD_CASE_APEX_CHANGE_CASE_WHEN_FORMATTING" value="true" />
<option name="KEYWORD_CASE_SOQL_SOSL_CHANGE_CASE_WHEN_FORMATTING" value="true" />
<option name="KEYWORD_CASE_VISUALFORCE_CHANGE_CASE_WHEN_FORMATTING" value="true" />
<option name="KEYWORD_CASE_AURA_CHANGE_CASE_WHEN_FORMATTING" value="true" />
<option name="APEX_DOC_REFORMAT_APEX_DOC" value="true" />
<option name="APEX_DOC_REQUIRE_DESCRIPTION_TAG" value="true" />
</ApexCodeStyleSettings>
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="Apex">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { registerSfCommands, clearCommandCache, refreshCommandCache, setProjectDirectory, getProjectRoots } from './sfCommands.js';
import path from 'path';
import { registerResources } from './resources.js';
import { extractProjectDirectoryFromMessage } from './utils.js';
// Create an MCP server
const server = new McpServer({
name: 'Salesforce CLI MCP',
version: '1.1.0',
description: 'MCP server for Salesforce CLI integration',
});
// Only register utility tools that aren't SF CLI commands
// These are utility functions that extend or manage the MCP server itself
server.tool('sf_cache_clear', 'Clear the cached SF command metadata to force a refresh', {}, async () => {
const result = clearCommandCache();
return {
content: [
{
type: 'text',
text: result
? 'Command cache cleared successfully.'
: 'Failed to clear command cache or cache did not exist.',
},
]
};
});
server.tool('sf_cache_refresh', 'Refresh the SF command cache by re-scanning all available commands', {}, async () => {
const result = refreshCommandCache();
return {
content: [
{
type: 'text',
text: result
? 'Command cache refreshed successfully. Restart the server to use the new cache.'
: 'Failed to refresh command cache.',
},
],
};
});
// Tools for managing Salesforce project directories (roots)
// Tool for automatically detecting project directories from messages
server.tool('sf_detect_project_directory', 'Get instructions for setting up Salesforce project directories for command execution', {}, async () => {
// Since we can't access the message in this version of MCP,
// we need to rely on the LLM to extract the directory and use sf_set_project_directory
return {
content: [
{
type: 'text',
text: 'To set a project directory, please use sf_set_project_directory with the path to your Salesforce project, or include the project path in your message using formats like "Execute in /path/to/project" or "Use project in /path/to/project".',
},
],
};
});
// Tool for explicitly setting a project directory (root)
server.tool('sf_set_project_directory', 'Set a Salesforce project directory for command execution context', {
directory: z.string().describe('The absolute path to a directory containing an sfdx-project.json file'),
name: z.string().optional().describe('Optional name for this project root'),
description: z.string().optional().describe('Optional description for this project root'),
isDefault: z.boolean().optional().describe('Set this root as the default for command execution')
}, async (params) => {
// Set the project directory with optional metadata
const result = setProjectDirectory(params.directory, {
name: params.name,
description: params.description,
isDefault: params.isDefault
});
return {
content: [
{
type: 'text',
text: result
? `Successfully set Salesforce project root: ${params.directory}${params.name ? ` with name "${params.name}"` : ''}${params.isDefault ? ' (default)' : ''}`
: `Failed to set project directory. Make sure the path exists and contains an sfdx-project.json file.`,
},
],
};
});
// Tool for listing configured project roots
server.tool('sf_list_roots', 'List all configured Salesforce project directories and their metadata', {}, async () => {
const roots = getProjectRoots();
if (roots.length === 0) {
return {
content: [
{
type: 'text',
text: 'No project roots configured. Use sf_set_project_directory to add a project root.'
}
]
};
}
// Format roots list for display
const rootsList = roots.map(root => (
`- ${root.name || path.basename(root.path)}${root.isDefault ? ' (default)' : ''}: ${root.path}${root.description ? `\n Description: ${root.description}` : ''}`
)).join('\n\n');
return {
content: [
{
type: 'text',
text: `Configured Salesforce project roots:\n\n${rootsList}`
}
]
};
});
// Start the server with stdio transport
// We can't use middleware, so we'll rely on explicit tool use
// The LLM will need to be instructed to look for project directory references
// and call the sf_set_project_directory tool
/**
* Process command line arguments to detect and set project roots
* All arguments that look like filesystem paths are treated as potential roots
*/
function processRootPaths(): void {
// Skip the first two arguments (node executable and script path)
const args = process.argv.slice(2);
if (!args || args.length === 0) {
console.error('No arguments provided');
return;
}
// Filter arguments that appear to be filesystem paths
// A path typically starts with / or ./ or ../ or ~/ or contains a directory separator
const rootPaths = args.filter(arg =>
arg.startsWith('/') ||
arg.startsWith('./') ||
arg.startsWith('../') ||
arg.startsWith('~/') ||
arg.includes('/') ||
arg.includes('\\')
);
if (rootPaths.length === 0) {
console.error('No project roots identified in CLI arguments');
return;
}
console.error(`Configuring ${rootPaths.length} project roots from CLI arguments...`);
// Process each provided path
for (let i = 0; i < rootPaths.length; i++) {
const rootPath = rootPaths[i];
const isDefault = i === 0; // Make the first root the default
const rootName = `root${i + 1}`;
// Set up this root
const result = setProjectDirectory(rootPath, {
name: rootName,
isDefault,
description: `CLI-configured root #${i + 1}`
});
if (result) {
console.error(`Configured project root #${i + 1}: ${rootPath}`);
} else {
console.error(`Failed to configure project root #${i + 1}: ${rootPath}`);
}
}
}
async function main() {
try {
// Process any command line arguments for project roots
processRootPaths();
// Register documentation resources
registerResources(server);
// Register all SF CLI commands as tools (dynamic discovery)
const dynamicToolCount = await registerSfCommands(server);
// Add the utility tools we registered manually
const totalTools = dynamicToolCount + 5; // sf_cache_clear, sf_cache_refresh, sf_set_project_directory, sf_detect_project_directory, sf_list_roots
console.error(`Total registered tools: ${totalTools} (${dynamicToolCount} SF CLI tools + 5 utility tools)`);
console.error('Starting Salesforce CLI MCP Server...');
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (err) {
console.error('Error starting server:', err);
process.exit(1);
}
}
main();
```
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="f109ddcd-c921-4a27-aea6-657a0d3b75de" name="Changes" comment="feat(context directories): All commands require a directory to execute from, so that the child_process can CWD there, and the sf cli understand it's project context">
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
"lastFilter": {
"state": "OPEN",
"assignee": "codefriar"
}
}</component>
<component name="GithubPullRequestsUISettings">{
"selectedUrlAndAccountId": {
"url": "[email protected]:codefriar/sf-mcp.git",
"accountId": "cd1051c7-86d1-42aa-9984-58b0635d53d5"
},
"recentNewPullRequestHead": {
"server": {
"useHttp": false,
"host": "github.com",
"port": null,
"suffix": null
},
"owner": "codefriar",
"repository": "sf-mcp"
}
}</component>
<component name="ProjectColorInfo">{
"associatedIndex": 4
}</component>
<component name="ProjectId" id="2urn5n86K5zpk5YZjhjYXG5kRcZ" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
"keyToString": {
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "#2 on feat/contextualExecution",
"js.linters.configure.manually.selectedeslint": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.standard": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "/Users/kpoorman/src/sfMcp/node_modules/@eslint/eslintrc",
"node.js.selected.package.standard": "",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "settings.javascript.linters.eslint",
"ts.external.directory.path": "/Applications/IntelliJ IDEA.app/Contents/plugins/javascript-plugin/jsLanguageServicesImpl/external",
"vue.rearranger.settings.migration": "true"
}
}</component>
<component name="RubyModuleManagerSettings">
<option name="blackListedRootsPaths">
<list>
<option value="$PROJECT_DIR$" />
</list>
</option>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-a94e463ab2e7-intellij.indexing.shared.core-IU-243.26053.27" />
<option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-IU-243.26053.27" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="f109ddcd-c921-4a27-aea6-657a0d3b75de" name="Changes" comment="" />
<created>1743015380248</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1743015380248</updated>
<workItem from="1743015381427" duration="2834000" />
<workItem from="1743030208472" duration="5343000" />
<workItem from="1743138461831" duration="925000" />
<workItem from="1743139755619" duration="9045000" />
<workItem from="1743222211272" duration="953000" />
<workItem from="1743606226833" duration="2474000" />
<workItem from="1743709358220" duration="3768000" />
</task>
<task id="LOCAL-00001" summary="feat(tools): autodiscovery of tools based on `sf commands --json`">
<option name="closed" value="true" />
<created>1743142354973</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1743142354973</updated>
</task>
<task id="LOCAL-00002" summary="chore(lint): prettier and eslint rules applied">
<option name="closed" value="true" />
<created>1743143924176</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1743143924176</updated>
</task>
<task id="LOCAL-00003" summary="chore(packaging): added standard-release">
<option name="closed" value="true" />
<created>1743144159287</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1743144159287</updated>
</task>
<task id="LOCAL-00004" summary="chore(build): build version">
<option name="closed" value="true" />
<created>1743144468869</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1743144468869</updated>
</task>
<task id="LOCAL-00005" summary="chore(cleanup): cleanup of code in prep for release, and the use of npx">
<option name="closed" value="true" />
<created>1743613695726</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1743613695726</updated>
</task>
<task id="LOCAL-00006" summary="feat(context directories): All commands require a directory to execute from, so that the child_process can CWD there, and the sf cli understand it's project context">
<option name="closed" value="true" />
<created>1743826235821</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1743826235821</updated>
</task>
<option name="localTasksCounter" value="7" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="feat(tools): autodiscovery of tools based on `sf commands --json`" />
<MESSAGE value="chore(lint): prettier and eslint rules applied" />
<MESSAGE value="chore(packaging): added standard-release" />
<MESSAGE value="chore(build): build version" />
<MESSAGE value="chore(cleanup): cleanup of code in prep for release, and the use of npx" />
<MESSAGE value="feat(context directories): All commands require a directory to execute from, so that the child_process can CWD there, and the sf cli understand it's project context" />
<option name="LAST_COMMIT_MESSAGE" value="feat(context directories): All commands require a directory to execute from, so that the child_process can CWD there, and the sf cli understand it's project context" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Utility functions for working with the Salesforce CLI
*/
/**
* Parse a user message to look for project directory specification
* @param message A message from the user that might contain project directory specification
* @returns The extracted directory path, or null if none found
*/
export function extractProjectDirectoryFromMessage(message: string): string | null {
if (!message) return null;
// Common patterns for specifying project directories
const patterns = [
// "Execute in /path/to/project"
/[Ee]xecute\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "Run in /path/to/project"
/[Rr]un\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "Use project in /path/to/project"
/[Uu]se\s+project\s+(?:in|from|at)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "Set project directory to /path/to/project"
/[Ss]et\s+project\s+directory\s+(?:to|as)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "Project is at /path/to/project"
/[Pp]roject\s+(?:is|located)\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "My project is in /path/to/project"
/[Mm]y\s+project\s+is\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/,
// "/path/to/project is my project"
/(['"]?)([\/~][^\n'"]+)\1\s+is\s+my\s+(?:project|directory)/,
];
for (const pattern of patterns) {
const match = message.match(pattern);
if (match) {
return match[2];
}
}
return null;
}
/**
* Formats an object as a string representation of CLI flags
* @param flags Key-value pairs of flag names and values
* @returns Formatted flags string suitable for command line
*/
export function formatFlags(flags: Record<string, any>): string {
if (!flags) return '';
return Object.entries(flags)
.map(([key, value]) => {
// Skip undefined/null values
if (value === undefined || value === null) return '';
// Handle boolean flags
if (typeof value === 'boolean') {
return value ? `--${key}` : '';
}
// Handle arrays (space-separated multi-values)
if (Array.isArray(value)) {
return value.map((v) => `--${key}=${escapeValue(v)}`).join(' ');
}
// Handle objects (JSON stringify)
if (typeof value === 'object') {
return `--${key}=${escapeValue(JSON.stringify(value))}`;
}
// Regular values
return `--${key}=${escapeValue(value)}`;
})
.filter(Boolean)
.join(' ');
}
/**
* Escapes values for command line usage
*/
function escapeValue(value: any): string {
const stringValue = String(value);
// If value contains spaces, wrap in quotes
if (stringValue.includes(' ')) {
// Escape any existing quotes
return `"${stringValue.replace(/"/g, '\\"')}"`;
}
return stringValue;
}
/**
* Parses help text to extract structured information about commands or flags
* @param helpText Help text from Salesforce CLI
* @returns Structured information extracted from help text
*/
export function parseHelpText(helpText: string): {
description: string;
examples: string[];
flags: Record<
string,
{
name: string;
description: string;
required: boolean;
type: string;
char?: string;
}
>;
} {
const description: string[] = [];
const examples: string[] = [];
const flags: Record<string, any> = {};
// Split by sections
const sections = helpText.split(/\n\s*\n/);
// Extract description (usually the first section, skipping DESCRIPTION header if present)
if (sections.length > 0) {
let firstSection = sections[0].trim();
if (firstSection.toUpperCase().startsWith('DESCRIPTION')) {
firstSection = firstSection.substring(firstSection.indexOf('\n') + 1).trim();
}
description.push(firstSection);
}
// Look for a description section if the first section wasn't clear
if (description[0]?.length < 10 || description[0]?.toUpperCase().includes('USAGE')) {
const descSection = sections.find(
(section) =>
section.toUpperCase().startsWith('DESCRIPTION') || section.toUpperCase().includes('\nDESCRIPTION\n')
);
if (descSection) {
const descContent = descSection.replace(/DESCRIPTION/i, '').trim();
if (descContent) {
description.push(descContent);
}
}
}
// Look for examples section with improved pattern matching
const examplePatterns = [/EXAMPLES?/i, /USAGE/i];
for (const pattern of examplePatterns) {
const exampleSection = sections.find((section) => pattern.test(section));
if (exampleSection) {
// Extract examples - look for command lines that start with $ or sf
const exampleLines = exampleSection
.split('\n')
.filter((line) => {
const trimmed = line.trim();
return trimmed.startsWith('$') || trimmed.startsWith('sf ') || /^\s*\d+\.\s+sf\s+/.test(line); // Numbered examples: "1. sf ..."
})
.map((line) => line.trim().replace(/^\d+\.\s+/, '')); // Remove numbering if present
examples.push(...exampleLines);
}
}
// Look for flags section with improved pattern matching
const flagPatterns = [/FLAGS/i, /OPTIONS/i, /PARAMETERS/i, /ARGUMENTS/i];
for (const pattern of flagPatterns) {
const flagSections = sections.filter((section) => pattern.test(section));
for (const flagSection of flagSections) {
// Skip the section header line
const sectionLines = flagSection.split('\n').slice(1);
// Different patterns for flag lines
const flagPatterns = [
// Pattern 1: Classic -c, --char=<value> Description
/^\s*(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:=<?([a-zA-Z0-9_\-\[\]|]+)>?)?\s+(.+)$/,
// Pattern 2: Indented flag with details (common in newer SF CLI)
/^\s+(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)(?:<([a-zA-Z0-9_\-\[\]|]+)>)?\s*\n\s+(.+)/,
// Pattern 3: Simple flag with no/minimal formatting
/^\s*(?:-([a-zA-Z]),\s*)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)?(?:\s*<([a-zA-Z0-9_\-\[\]|]+)>)?\s+(.+)$/,
];
// Process the flag section
let i = 0;
while (i < sectionLines.length) {
const line = sectionLines[i];
const nextLine = i < sectionLines.length - 1 ? sectionLines[i + 1] : '';
const combinedLines = line + '\n' + nextLine;
let matched = false;
// Try all patterns
for (const pattern of flagPatterns) {
const match = line.match(pattern) || combinedLines.match(pattern);
if (match) {
matched = true;
const char = match[1];
const name = match[2];
const type = match[3] || 'boolean';
const description = match[4].trim();
// Check if this flag is required
const required =
description.toLowerCase().includes('(required)') ||
description.toLowerCase().includes('[required]') ||
description.toLowerCase().includes('required:') ||
description.toLowerCase().includes('required -');
// Normalize the type
let normalizedType = type.toLowerCase();
if (normalizedType.includes('number') || normalizedType.includes('int')) {
normalizedType = 'number';
} else if (normalizedType.includes('boolean') || normalizedType === 'flag') {
normalizedType = 'boolean';
} else if (normalizedType.includes('array') || normalizedType.includes('[]')) {
normalizedType = 'array';
} else if (normalizedType.includes('json') || normalizedType.includes('object')) {
normalizedType = 'json';
} else {
normalizedType = 'string';
}
flags[name] = {
name,
char,
description: description
.replace(/\([Rr]equired\)|\[[Rr]equired\]|[Rr]equired:?/g, '')
.trim(),
required,
type: normalizedType,
};
// Skip the next line if we matched against a two-line pattern
if (combinedLines.match(pattern) && !line.match(pattern)) {
i++;
}
break;
}
}
// If no pattern matched and this line looks like it might be a flag
if (!matched && (line.includes('--') || line.trim().startsWith('-'))) {
console.error(`No pattern matched for potential flag line: "${line.trim()}"`);
}
i++;
}
}
}
return {
description: description.join('\n\n'),
examples,
flags,
};
}
```
--------------------------------------------------------------------------------
/.idea/AugmentWebviewStateStore.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AugmentWebviewStateStore">
<option name="stateMap">
<map>
<entry key="CHAT_STATE" value="eyJjdXJyZW50Q29udmVyc2F0aW9uSWQiOiJiOTcyZWQwZi0yNmUzLTQ4NjktOTY4YS01YzEyZTIyOTZhNzEiLCJjb252ZXJzYXRpb25zIjp7ImI5NzJlZDBmLTI2ZTMtNDg2OS05NjhhLTVjMTJlMjI5NmE3MSI6eyJpZCI6ImI5NzJlZDBmLTI2ZTMtNDg2OS05NjhhLTVjMTJlMjI5NmE3MSIsImNyZWF0ZWRBdElzbyI6IjIwMjUtMDQtMDhUMDU6NDI6MDIuOTIzWiIsImxhc3RJbnRlcmFjdGVkQXRJc28iOiIyMDI1LTA0LTA4VDA1OjQzOjQxLjUxMFoiLCJjaGF0SGlzdG9yeSI6W3sic3RhdHVzIjoic3VjY2VzcyIsInJlcXVlc3RfaWQiOiJjODczNTEyMS1lMzkwLTRkNWItYmE0YS0zODJmMTg0NGY5MGUiLCJyZXF1ZXN0X21lc3NhZ2UiOiJQbGVhc2Ugc2NhbiB0aGUgcHJvamVjdCwgYW5kIHVwZGF0ZSB0aGUgUkVBRE1FIGFwcHJvcHJpYXRlbHkuIE1ha2Ugc3VyZSB0byB1cGRhdGUgdGhlIGluc3RhbGxhdGlvbiBpbnN0cnVjdGlvbnMgYW5kIGNvZGUgdG8gc3VwcG9ydCBydW5uaW5nIHZpYSBucHggYW5kIGluc3RhbGxpbmcgdmlhIG5wbSAiLCJyaWNoX3RleHRfanNvbl9yZXByIjp7InR5cGUiOiJkb2MiLCJjb250ZW50IjpbeyJ0eXBlIjoicGFyYWdyYXBoIiwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0IjoiUGxlYXNlIHNjYW4gdGhlIHByb2plY3QsIGFuZCB1cGRhdGUgdGhlIFJFQURNRSBhcHByb3ByaWF0ZWx5LiBNYWtlIHN1cmUgdG8gdXBkYXRlIHRoZSBpbnN0YWxsYXRpb24gaW5zdHJ1Y3Rpb25zIGFuZCBjb2RlIHRvIHN1cHBvcnQgcnVubmluZyB2aWEgbnB4IGFuZCBpbnN0YWxsaW5nIHZpYSBucG0gIn1dfV19LCJtZW50aW9uZWRfaXRlbXMiOltdLCJzdHJ1Y3R1cmVkX291dHB1dF9ub2RlcyI6W3siaWQiOjAsInR5cGUiOjAsImNvbnRlbnQiOiJCYXNlZCBvbiB0aGUgY29kZWJhc2UsIEknbGwgdXBkYXRlIHRoZSBSRUFETUUgdG8gaW5jbHVkZSBwcm9wZXIgaW5zdGFsbGF0aW9uIGluc3RydWN0aW9ucyBmb3IgcnVubmluZyB2aWEgbnB4IGFuZCBpbnN0YWxsaW5nIHZpYSBucG0uIFRoZSBjdXJyZW50IFJFQURNRSBhbHJlYWR5IGhhcyBzb21lIG9mIHRoaXMgaW5mb3JtYXRpb24sIGJ1dCBJJ2xsIGVuaGFuY2UgaXQgdG8gYmUgbW9yZSBjb21wbGV0ZSBhbmQgYWNjdXJhdGUuXG5cbjxhdWdtZW50X2NvZGVfc25pcHBldCBwYXRoPVwiUkVBRE1FLm1kXCIgbW9kZT1cIkVESVRcIj5cbmBgYG1hcmtkb3duXG4jIFNhbGVzZm9yY2UgQ0xJIE1DUCBTZXJ2ZXJcblxuTW9kZWwgQ29udGV4dCBQcm90b2NvbCAoTUNQKSBzZXJ2ZXIgZm9yIHByb3ZpZGluZyBTYWxlc2ZvcmNlIENMSSBmdW5jdGlvbmFsaXR5IHRvIExMTSB0b29scyBsaWtlIENsYXVkZSBEZXNrdG9wLlxuXG4jIyBPdmVydmlld1xuXG5UaGlzIE1DUCBzZXJ2ZXIgd3JhcHMgdGhlIFNhbGVzZm9yY2UgQ0xJIChgc2ZgKSBjb21tYW5kLWxpbmUgdG9vbCBhbmQgZXhwb3NlcyBpdHMgY29tbWFuZHMgYXMgTUNQIHRvb2xzIGFuZCByZXNvdXJjZXMsIGFsbG93aW5nIExMTS1wb3dlcmVkIGFnZW50cyB0bzpcblxuLSBWaWV3IGhlbHAgaW5mb3JtYXRpb24gYWJvdXQgU2FsZXNmb3JjZSBDTEkgdG9waWNzIGFuZCBjb21tYW5kc1xuLSBFeGVjdXRlIFNhbGVzZm9yY2UgQ0xJIGNvbW1hbmRzIHdpdGggYXBwcm9wcmlhdGUgcGFyYW1ldGVyc1xuLSBMZXZlcmFnZSBTYWxlc2ZvcmNlIENMSSBjYXBhYmlsaXRpZXMgaW4gQUkgd29ya2Zsb3dzXG5cbiMjIFJlcXVpcmVtZW50c1xuXG4tIE5vZGUuanMgMTgrIGFuZCBucG1cbi0gU2FsZXNmb3JjZSBDTEkgKGBzZmApIGluc3RhbGxlZCBhbmQgY29uZmlndXJlZFxuLSBZb3VyIFNhbGVzZm9yY2Ugb3JnIGNyZWRlbnRpYWxzIGNvbmZpZ3VyZWQgaW4gdGhlIENMSVxuXG4jIyBJbnN0YWxsYXRpb25cblxuIyMjIE9wdGlvbiAxOiBJbnN0YWxsIGZyb20gbnBtIChyZWNvbW1lbmRlZClcblxuYGBgYmFzaFxuIyBJbnN0YWxsIGdsb2JhbGx5XG5ucG0gaW5zdGFsbCAtZyBzZi1tY3BcblxuIyBPciBydW4gZGlyZWN0bHkgd2l0aCBucHhcbm5weCBzZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5gYGBcblxuIyMjIE9wdGlvbiAyOiBDbG9uZSBhbmQgYnVpbGRcblxuYGBgYmFzaFxuIyBDbG9uZSB0aGUgcmVwb3NpdG9yeVxuZ2l0IGNsb25lIDxyZXBvc2l0b3J5LXVybD5cbmNkIHNmLW1jcFxuXG4jIEluc3RhbGwgZGVwZW5kZW5jaWVzXG5ucG0gaW5zdGFsbFxuXG4jIEJ1aWxkIHRoZSBwcm9qZWN0XG5ucG0gcnVuIGJ1aWxkXG5gYGBcblxuIyMgVXNhZ2VcblxuIyMjIFN0YXJ0aW5nIHRoZSBzZXJ2ZXJcblxuYGBgYmFzaFxuIyBJZiBpbnN0YWxsZWQgZ2xvYmFsbHlcbnNmLW1jcCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBJZiB1c2luZyBucHhcbm5weCBzZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgSWYgY2xvbmVkIGxvY2FsbHlcbm5wbSBzdGFydCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBXaXRoIHNwZWNpZmljIHByb2plY3Qgcm9vdHNcbnNmLW1jcCAvcGF0aC90by9wcm9qZWN0MSAvcGF0aC90by9wcm9qZWN0MlxuIyBvciB1c2luZyB0aGUgY29udmVuaWVuY2Ugc2NyaXB0IGlmIGNsb25lZCBsb2NhbGx5XG5ucG0gcnVuIHdpdGgtcm9vdHMgL3BhdGgvdG8vcHJvamVjdDEgL3BhdGgvdG8vcHJvamVjdDJcbmBgYFxuXG5UaGUgTUNQIHNlcnZlciB1c2VzIHN0ZGlvIHRyYW5zcG9ydCwgd2hpY2ggY2FuIGJlIHVzZWQgd2l0aCBNQ1AgY2xpZW50cyBsaWtlIHRoZSBbTUNQIEluc3BlY3Rvcl0oaHR0cHM6Ly9naXRodWIuY29tL21vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3Rvcikgb3IgQ2xhdWRlIERlc2t0b3AuXG5cbiMjIyBUZXN0aW5nIHdpdGggTUNQIEluc3BlY3RvclxuXG5Zb3UgY2FuIHRlc3QgdGhlIHNlcnZlciB1c2luZyB0aGUgTUNQIEluc3BlY3RvcjpcblxuYGBgYmFzaFxuIyBUZXN0IHdpdGggSW5zcGVjdG9yXG5ucHggQG1vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3RvciBucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuXG4jIE9yIGlmIGNsb25lZCBsb2NhbGx5XG5ucHggQG1vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3RvciBub2RlIGJ1aWxkL2luZGV4LmpzIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuYGBgXG5cbiMjIyBDb25maWd1cmluZyBpbiBDbGF1ZGUgRGVza3RvcFxuXG5UbyBjb25maWd1cmUgdGhpcyBNQ1AgaW4gQ2xhdWRlIERlc2t0b3AncyBgLmNsYXVkZS5qc29uYCBjb25maWd1cmF0aW9uOlxuXG5gYGBqc29uXG57XG4gIFwidG9vbHNcIjoge1xuICAgIFwic2FsZXNmb3JjZVwiOiB7XG4gICAgICBcImNvbW1hbmRcIjogXCJucHhcIixcbiAgICAgIFwiYXJnc1wiOiBbXG4gICAgICAgIFwiLXlcIixcbiAgICAgICAgXCJzZi1tY3BcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MVwiLFxuICAgICAgICBcIi9wYXRoL3RvL3Byb2plY3QyXCJcbiAgICAgIF1cbiAgICB9XG4gIH1cbn1cbmBgYFxuXG5Vc2luZyBhIGxvY2FsbHkgY2xvbmVkIHZlcnNpb246XG5cbmBgYGpzb25cbntcbiAgXCJ0b29sc1wiOiB7XG4gICAgXCJzYWxlc2ZvcmNlXCI6IHtcbiAgICAgIFwiY29tbWFuZFwiOiBcIi9wYXRoL3RvL25vZGVcIixcbiAgICAgIFwiYXJnc1wiOiBbXG4gICAgICAgIFwiL3BhdGgvdG8vc2YtbWNwL2J1aWxkL2luZGV4LmpzXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDFcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MlwiXG4gICAgICBdXG4gICAgfVxuICB9XG59XG5gYGBcblxuIyMjIERldmVsb3BtZW50XG5cbmBgYGJhc2hcbiMgV2F0Y2ggbW9kZSAocmVjb21waWxlcyBvbiBmaWxlIGNoYW5nZXMpXG5ucG0gcnVuIGRldlxuXG4jIEluIGFub3RoZXIgdGVybWluYWxcbm5wbSBzdGFydCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cbmBgYFxuYGBgXG48L2F1Z21lbnRfY29kZV9zbmlwcGV0PlxuXG5JJ3ZlIHVwZGF0ZWQgdGhlIFJFQURNRSB3aXRoIGNvbXByZWhlbnNpdmUgaW5zdGFsbGF0aW9uIGFuZCB1c2FnZSBpbnN0cnVjdGlvbnMgdGhhdCBzdXBwb3J0OlxuXG4xLiBJbnN0YWxsaW5nIGdsb2JhbGx5IHZpYSBucG0gKGBucG0gaW5zdGFsbCAtZyBzZi1tY3BgKVxuMi4gUnVubmluZyBkaXJlY3RseSB3aXRoIG5weCAoYG5weCBzZi1tY3BgKVxuMy4gQ2xvbmluZyBhbmQgYnVpbGRpbmcgbG9jYWxseVxuNC4gVGVzdGluZyB3aXRoIHRoZSBNQ1AgSW5zcGVjdG9yXG41LiBDb25maWd1cmluZyBpbiBDbGF1ZGUgRGVza3RvcCB3aXRoIGJvdGggbnB4IGFuZCBsb2NhbCBvcHRpb25zXG5cblRoZSBpbnN0cnVjdGlvbnMgbWFpbnRhaW4gYWxsIHRoZSBleGlzdGluZyBmdW5jdGlvbmFsaXR5IHdoaWxlIG1ha2luZyBpdCBjbGVhciBob3cgdG8gdXNlIHRoZSBwYWNrYWdlIHZpYSBucG0gYW5kIG5weC4gSSd2ZSBhbHNvIG9yZ2FuaXplZCB0aGUgaW5zdGFsbGF0aW9uIG9wdGlvbnMgdG8gaGlnaGxpZ2h0IHRoZSBucG0gaW5zdGFsbGF0aW9uIGFzIHRoZSByZWNvbW1lbmRlZCBhcHByb2FjaCBmb3IgbW9zdCB1c2Vycy5cbiJ9LHsiaWQiOjIsInR5cGUiOjIsImNvbnRlbnQiOiIifSx7ImlkIjozLCJ0eXBlIjozLCJjb250ZW50IjoiUkVBRE1FLm1kOjAtNzYwXG5SRUFETUUubWQ6NzYwLTEzODhcblJFQURNRS5tZDoxMzg4LTIwNDFcblJFQURNRS5tZDoyMDQxLTI1NDFcblJFQURNRS5tZDoyNTQxLTMxNDhcblJFQURNRS5tZDozMTQ4LTM2MDVcblJFQURNRS5tZDozNjA1LTQyOTVcblJFQURNRS5tZDo0Mjk1LTQ4NjJcblJFQURNRS5tZDo0ODYyLTU1ODdcblJFQURNRS5tZDo1NTg3LTYyMThcblJFQURNRS5tZDo2MjE4LTY5NjZcblJFQURNRS5tZDo2OTY2LTc2MTRcblJFQURNRS5tZDo3NjE0LTc3OTlcbmxsbXMtZnVsbC50eHQ6MjQ5MTAtMjU1NzJcbmxsbXMtZnVsbC50eHQ6OTIxNDktOTI4NjJcbmxsbXMtZnVsbC50eHQ6OTI4NjItOTM1NjRcbmxsbXMtZnVsbC50eHQ6OTM1NjQtOTQyODJcbmxsbXMtZnVsbC50eHQ6MTAxMzA4LTEwMjAwOFxubGxtcy1mdWxsLnR4dDoxMDI0NzYtMTAzMjIzXG5sbG1zLWZ1bGwudHh0OjEwOTEyNy0xMDk5ODRcbmxsbXMtZnVsbC50eHQ6MTE3Nzk1LTExODYwNlxubGxtcy1mdWxsLnR4dDoxMjE3MDktMTIyNTY2XG5sbG1zLWZ1bGwudHh0OjEyODk5OS0xMjk3NjVcbmxsbXMtZnVsbC50eHQ6MTMzMTcxLTEzNDA5MlxubGxtcy1mdWxsLnR4dDoxMzYxMjItMTM2ODA4XG5sbG1zLWZ1bGwudHh0OjE0NzI1NC0xNDc5MTRcbmxsbXMtZnVsbC50eHQ6MTU2MTU5LTE1Njk3MFxubGxtcy1mdWxsLnR4dDoxNjA0MjctMTYwNjc5XG5sbG1zLWZ1bGwudHh0OjE2MDY3OS0xNjE1MzBcbmxsbXMtZnVsbC50eHQ6MTY5NDQyLTE3MDI1M1xubGxtcy1mdWxsLnR4dDoxODU1NDYtMTg2MTA5XG5sbG1zLWZ1bGwudHh0OjIwNDQ2OS0yMDUyNzFcbmxsbXMtZnVsbC50eHQ6MjA1MjcxLTIwNjAyOFxubGxtcy1mdWxsLnR4dDoyMjA4MDktMjIxNDQxXG5sbG1zLWZ1bGwudHh0OjIyNjA2NC0yMjY1NDJcbkNMQVVERS5tZDowLTM4NlxucnVuLnNoOjAtNDgwXG5zcmMvaW5kZXgudHM6MC02MDVcbnNyYy9pbmRleC50czozMjAwLTQwNDMifV0sInNlZW5fc3RhdGUiOiJzZWVuIiwicmVzcG9uc2VfdGV4dCI6IkJhc2VkIG9uIHRoZSBjb2RlYmFzZSwgSSdsbCB1cGRhdGUgdGhlIFJFQURNRSB0byBpbmNsdWRlIHByb3BlciBpbnN0YWxsYXRpb24gaW5zdHJ1Y3Rpb25zIGZvciBydW5uaW5nIHZpYSBucHggYW5kIGluc3RhbGxpbmcgdmlhIG5wbS4gVGhlIGN1cnJlbnQgUkVBRE1FIGFscmVhZHkgaGFzIHNvbWUgb2YgdGhpcyBpbmZvcm1hdGlvbiwgYnV0IEknbGwgZW5oYW5jZSBpdCB0byBiZSBtb3JlIGNvbXBsZXRlIGFuZCBhY2N1cmF0ZS5cblxuYGBgYG1hcmtkb3duIHBhdGg9UkVBRE1FLm1kIG1vZGU9RURJVFxuIyBTYWxlc2ZvcmNlIENMSSBNQ1AgU2VydmVyXG5cbk1vZGVsIENvbnRleHQgUHJvdG9jb2wgKE1DUCkgc2VydmVyIGZvciBwcm92aWRpbmcgU2FsZXNmb3JjZSBDTEkgZnVuY3Rpb25hbGl0eSB0byBMTE0gdG9vbHMgbGlrZSBDbGF1ZGUgRGVza3RvcC5cblxuIyMgT3ZlcnZpZXdcblxuVGhpcyBNQ1Agc2VydmVyIHdyYXBzIHRoZSBTYWxlc2ZvcmNlIENMSSAoYHNmYCkgY29tbWFuZC1saW5lIHRvb2wgYW5kIGV4cG9zZXMgaXRzIGNvbW1hbmRzIGFzIE1DUCB0b29scyBhbmQgcmVzb3VyY2VzLCBhbGxvd2luZyBMTE0tcG93ZXJlZCBhZ2VudHMgdG86XG5cbi0gVmlldyBoZWxwIGluZm9ybWF0aW9uIGFib3V0IFNhbGVzZm9yY2UgQ0xJIHRvcGljcyBhbmQgY29tbWFuZHNcbi0gRXhlY3V0ZSBTYWxlc2ZvcmNlIENMSSBjb21tYW5kcyB3aXRoIGFwcHJvcHJpYXRlIHBhcmFtZXRlcnNcbi0gTGV2ZXJhZ2UgU2FsZXNmb3JjZSBDTEkgY2FwYWJpbGl0aWVzIGluIEFJIHdvcmtmbG93c1xuXG4jIyBSZXF1aXJlbWVudHNcblxuLSBOb2RlLmpzIDE4KyBhbmQgbnBtXG4tIFNhbGVzZm9yY2UgQ0xJIChgc2ZgKSBpbnN0YWxsZWQgYW5kIGNvbmZpZ3VyZWRcbi0gWW91ciBTYWxlc2ZvcmNlIG9yZyBjcmVkZW50aWFscyBjb25maWd1cmVkIGluIHRoZSBDTElcblxuIyMgSW5zdGFsbGF0aW9uXG5cbiMjIyBPcHRpb24gMTogSW5zdGFsbCBmcm9tIG5wbSAocmVjb21tZW5kZWQpXG5cbmBgYGJhc2hcbiMgSW5zdGFsbCBnbG9iYWxseVxubnBtIGluc3RhbGwgLWcgc2YtbWNwXG5cbiMgT3IgcnVuIGRpcmVjdGx5IHdpdGggbnB4XG5ucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuYGBgXG5cbiMjIyBPcHRpb24gMjogQ2xvbmUgYW5kIGJ1aWxkXG5cbmBgYGJhc2hcbiMgQ2xvbmUgdGhlIHJlcG9zaXRvcnlcbmdpdCBjbG9uZSA8cmVwb3NpdG9yeS11cmw+XG5jZCBzZi1tY3BcblxuIyBJbnN0YWxsIGRlcGVuZGVuY2llc1xubnBtIGluc3RhbGxcblxuIyBCdWlsZCB0aGUgcHJvamVjdFxubnBtIHJ1biBidWlsZFxuYGBgXG5cbiMjIFVzYWdlXG5cbiMjIyBTdGFydGluZyB0aGUgc2VydmVyXG5cbmBgYGJhc2hcbiMgSWYgaW5zdGFsbGVkIGdsb2JhbGx5XG5zZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgSWYgdXNpbmcgbnB4XG5ucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuXG4jIElmIGNsb25lZCBsb2NhbGx5XG5ucG0gc3RhcnQgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgV2l0aCBzcGVjaWZpYyBwcm9qZWN0IHJvb3RzXG5zZi1tY3AgL3BhdGgvdG8vcHJvamVjdDEgL3BhdGgvdG8vcHJvamVjdDJcbiMgb3IgdXNpbmcgdGhlIGNvbnZlbmllbmNlIHNjcmlwdCBpZiBjbG9uZWQgbG9jYWxseVxubnBtIHJ1biB3aXRoLXJvb3RzIC9wYXRoL3RvL3Byb2plY3QxIC9wYXRoL3RvL3Byb2plY3QyXG5gYGBcblxuVGhlIE1DUCBzZXJ2ZXIgdXNlcyBzdGRpbyB0cmFuc3BvcnQsIHdoaWNoIGNhbiBiZSB1c2VkIHdpdGggTUNQIGNsaWVudHMgbGlrZSB0aGUgW01DUCBJbnNwZWN0b3JdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3IpIG9yIENsYXVkZSBEZXNrdG9wLlxuXG4jIyMgVGVzdGluZyB3aXRoIE1DUCBJbnNwZWN0b3JcblxuWW91IGNhbiB0ZXN0IHRoZSBzZXJ2ZXIgdXNpbmcgdGhlIE1DUCBJbnNwZWN0b3I6XG5cbmBgYGJhc2hcbiMgVGVzdCB3aXRoIEluc3BlY3RvclxubnB4IEBtb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3IgbnB4IHNmLW1jcCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBPciBpZiBjbG9uZWQgbG9jYWxseVxubnB4IEBtb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3Igbm9kZSBidWlsZC9pbmRleC5qcyBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cbmBgYFxuXG4jIyMgQ29uZmlndXJpbmcgaW4gQ2xhdWRlIERlc2t0b3BcblxuVG8gY29uZmlndXJlIHRoaXMgTUNQIGluIENsYXVkZSBEZXNrdG9wJ3MgYC5jbGF1ZGUuanNvbmAgY29uZmlndXJhdGlvbjpcblxuYGBganNvblxue1xuICBcInRvb2xzXCI6IHtcbiAgICBcInNhbGVzZm9yY2VcIjoge1xuICAgICAgXCJjb21tYW5kXCI6IFwibnB4XCIsXG4gICAgICBcImFyZ3NcIjogW1xuICAgICAgICBcIi15XCIsXG4gICAgICAgIFwic2YtbWNwXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDFcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MlwiXG4gICAgICBdXG4gICAgfVxuICB9XG59XG5gYGBcblxuVXNpbmcgYSBsb2NhbGx5IGNsb25lZCB2ZXJzaW9uOlxuXG5gYGBqc29uXG57XG4gIFwidG9vbHNcIjoge1xuICAgIFwic2FsZXNmb3JjZVwiOiB7XG4gICAgICBcImNvbW1hbmRcIjogXCIvcGF0aC90by9ub2RlXCIsXG4gICAgICBcImFyZ3NcIjogW1xuICAgICAgICBcIi9wYXRoL3RvL3NmLW1jcC9idWlsZC9pbmRleC5qc1wiLFxuICAgICAgICBcIi9wYXRoL3RvL3Byb2plY3QxXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDJcIlxuICAgICAgXVxuICAgIH1cbiAgfVxufVxuYGBgXG5cbiMjIyBEZXZlbG9wbWVudFxuXG5gYGBiYXNoXG4jIFdhdGNoIG1vZGUgKHJlY29tcGlsZXMgb24gZmlsZSBjaGFuZ2VzKVxubnBtIHJ1biBkZXZcblxuIyBJbiBhbm90aGVyIHRlcm1pbmFsXG5ucG0gc3RhcnQgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5gYGBcbmBgYGBcblxuSSd2ZSB1cGRhdGVkIHRoZSBSRUFETUUgd2l0aCBjb21wcmVoZW5zaXZlIGluc3RhbGxhdGlvbiBhbmQgdXNhZ2UgaW5zdHJ1Y3Rpb25zIHRoYXQgc3VwcG9ydDpcblxuMS4gSW5zdGFsbGluZyBnbG9iYWxseSB2aWEgbnBtIChgbnBtIGluc3RhbGwgLWcgc2YtbWNwYClcbjIuIFJ1bm5pbmcgZGlyZWN0bHkgd2l0aCBucHggKGBucHggc2YtbWNwYClcbjMuIENsb25pbmcgYW5kIGJ1aWxkaW5nIGxvY2FsbHlcbjQuIFRlc3Rpbmcgd2l0aCB0aGUgTUNQIEluc3BlY3RvclxuNS4gQ29uZmlndXJpbmcgaW4gQ2xhdWRlIERlc2t0b3Agd2l0aCBib3RoIG5weCBhbmQgbG9jYWwgb3B0aW9uc1xuXG5UaGUgaW5zdHJ1Y3Rpb25zIG1haW50YWluIGFsbCB0aGUgZXhpc3RpbmcgZnVuY3Rpb25hbGl0eSB3aGlsZSBtYWtpbmcgaXQgY2xlYXIgaG93IHRvIHVzZSB0aGUgcGFja2FnZSB2aWEgbnBtIGFuZCBucHguIEkndmUgYWxzbyBvcmdhbml6ZWQgdGhlIGluc3RhbGxhdGlvbiBvcHRpb25zIHRvIGhpZ2hsaWdodCB0aGUgbnBtIGluc3RhbGxhdGlvbiBhcyB0aGUgcmVjb21tZW5kZWQgYXBwcm9hY2ggZm9yIG1vc3QgdXNlcnMuXG4iLCJ3b3Jrc3BhY2VfZmlsZV9jaHVua3MiOlt7ImNoYXJfc3RhcnQiOiIwIiwiY2hhcl9lbmQiOiI3NjAiLCJibG9iX25hbWUiOiIwZDY2OWYwNjRjM2I4OTcyNWFiZTYyYmI5YmU2N2JmM2UwZDQzMGVjOWIzYjlmMWExNzBlNTdjMjdjOTAxYzE0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoiUkVBRE1FLm1kIn19LHsiY2hhcl9zdGFydCI6Ijc2MCIsImNoYXJfZW5kIjoiMTM4OCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMTM4OCIsImNoYXJfZW5kIjoiMjA0MSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjA0MSIsImNoYXJfZW5kIjoiMjU0MSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjU0MSIsImNoYXJfZW5kIjoiMzE0OCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMzE0OCIsImNoYXJfZW5kIjoiMzYwNSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMzYwNSIsImNoYXJfZW5kIjoiNDI5NSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNDI5NSIsImNoYXJfZW5kIjoiNDg2MiIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNDg2MiIsImNoYXJfZW5kIjoiNTU4NyIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNTU4NyIsImNoYXJfZW5kIjoiNjIxOCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNjIxOCIsImNoYXJfZW5kIjoiNjk2NiIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNjk2NiIsImNoYXJfZW5kIjoiNzYxNCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNzYxNCIsImNoYXJfZW5kIjoiNzc5OSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjQ5MTAiLCJjaGFyX2VuZCI6IjI1NTcyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTIxNDkiLCJjaGFyX2VuZCI6IjkyODYyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTI4NjIiLCJjaGFyX2VuZCI6IjkzNTY0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTM1NjQiLCJjaGFyX2VuZCI6Ijk0MjgyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTAxMzA4IiwiY2hhcl9lbmQiOiIxMDIwMDgiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMDI0NzYiLCJjaGFyX2VuZCI6IjEwMzIyMyIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjEwOTEyNyIsImNoYXJfZW5kIjoiMTA5OTg0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTE3Nzk1IiwiY2hhcl9lbmQiOiIxMTg2MDYiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMjE3MDkiLCJjaGFyX2VuZCI6IjEyMjU2NiIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjEyODk5OSIsImNoYXJfZW5kIjoiMTI5NzY1IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTMzMTcxIiwiY2hhcl9lbmQiOiIxMzQwOTIiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMzYxMjIiLCJjaGFyX2VuZCI6IjEzNjgwOCIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjE0NzI1NCIsImNoYXJfZW5kIjoiMTQ3OTE0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTU2MTU5IiwiY2hhcl9lbmQiOiIxNTY5NzAiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxNjA0MjciLCJjaGFyX2VuZCI6IjE2MDY3OSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjE2MDY3OSIsImNoYXJfZW5kIjoiMTYxNTMwIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTY5NDQyIiwiY2hhcl9lbmQiOiIxNzAyNTMiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxODU1NDYiLCJjaGFyX2VuZCI6IjE4NjEwOSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjIwNDQ2OSIsImNoYXJfZW5kIjoiMjA1MjcxIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMjA1MjcxIiwiY2hhcl9lbmQiOiIyMDYwMjgiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIyMjA4MDkiLCJjaGFyX2VuZCI6IjIyMTQ0MSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjIyNjA2NCIsImNoYXJfZW5kIjoiMjI2NTQyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMCIsImNoYXJfZW5kIjoiMzg2IiwiYmxvYl9uYW1lIjoiMTFjMjk0MDU4YmM0NTA3MWExZTM3NGRjMzFmNTBlMWM4ZTRjMTBiZDE0ZmEwYWZiNDRkZWE1MmRkZmNkYWYxOSIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6IkNMQVVERS5tZCJ9fSx7ImNoYXJfc3RhcnQiOiIwIiwiY2hhcl9lbmQiOiI0ODAiLCJibG9iX25hbWUiOiJhMDMwODQ3YzgwNmMwZDYxYjkzYzIwZjQyZjI1ZDMwMjZiMzBjOWY0NGY5ZWU0ZGE4OTlkYTRjMjY5MTYyYmY0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoicnVuLnNoIn19LHsiY2hhcl9zdGFydCI6IjAiLCJjaGFyX2VuZCI6IjYwNSIsImJsb2JfbmFtZSI6IjliYTkwYzZmZGRkYjM0NDU2YWY0Y2M4YWQ2MzVmMTVjNjhjMDQ5ZDhhZGI4MzZhZDYwM2E0YWM3MTFmODYxMmQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJzcmMvaW5kZXgudHMifX0seyJjaGFyX3N0YXJ0IjoiMzIwMCIsImNoYXJfZW5kIjoiNDA0MyIsImJsb2JfbmFtZSI6IjliYTkwYzZmZGRkYjM0NDU2YWY0Y2M4YWQ2MzVmMTVjNjhjMDQ5ZDhhZGI4MzZhZDYwM2E0YWM3MTFmODYxMmQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJzcmMvaW5kZXgudHMifX1dfV0sImZlZWRiYWNrU3RhdGVzIjp7InRlbXAtZmUtNWRmNTU4ZGUtMjk0MC00MzFiLTgyOGEtYTA5NzFjMTk1Y2FmIjp7InNlbGVjdGVkUmF0aW5nIjowLCJmZWVkYmFja05vdGUiOiIifX0sInRvb2xVc2VTdGF0ZXMiOnt9LCJkcmFmdEV4Y2hhbmdlIjp7InJlcXVlc3RfbWVzc2FnZSI6IiIsInJpY2hfdGV4dF9qc29uX3JlcHIiOnsidHlwZSI6ImRvYyIsImNvbnRlbnQiOlt7InR5cGUiOiJwYXJhZ3JhcGgifV19LCJzdGF0dXMiOiJkcmFmdCJ9LCJyZXF1ZXN0SWRzIjpbXSwiaXNQaW5uZWQiOmZhbHNlLCJpc1NoYXJlYWJsZSI6dHJ1ZSwiZXh0cmFEYXRhIjp7Imhhc0RpcnR5RWRpdHMiOmZhbHNlfX19LCJpc0NvbnRleHRQYW5lT3BlbiI6ZmFsc2UsImlzQWN0aW9uc1BhbmVPcGVuIjpmYWxzZSwiYWdlbnRFeGVjdXRpb25Nb2RlIjoibWFudWFsIiwiaXNBZ2VudEVkaXRzQ29sbGFwc2VkIjp0cnVlfQ==" />
</map>
</option>
</component>
</project>
```
--------------------------------------------------------------------------------
/src/sfCommands.ts:
--------------------------------------------------------------------------------
```typescript
import { execSync } from 'child_process';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { formatFlags } from './utils.js';
import fs from 'fs';
import path from 'path';
import os from 'os';
/**
* Represents a Salesforce CLI command
*/
interface SfCommand {
id: string;
name: string;
description: string;
fullCommand: string;
flags: SfFlag[];
topic?: string;
}
/**
* Represents a flag for an SF command
*/
interface SfFlag {
name: string;
char?: string;
description: string;
required: boolean;
type: string;
options?: string[];
default?: string | boolean | number;
}
/**
* Interface for the JSON format returned by 'sf commands --json'
*/
interface SfCommandJsonEntry {
id: string;
summary: string;
description: string;
aliases?: string[];
flags: Record<
string,
{
name: string;
description: string;
type: string;
required?: boolean;
helpGroup?: string;
options?: string[];
default?: string | boolean | number;
char?: string;
}
>;
[key: string]: any;
}
/**
* Cache structure for storing discovered SF commands
*/
interface SfCommandCache {
version: string;
timestamp: number;
commands: SfCommand[];
}
/**
* List of topics to ignore during command discovery
*/
const IGNORED_TOPICS = ['help', 'which', 'whatsnew', 'alias'];
/**
* Path to the cache file
*/
const CACHE_DIR = path.join(os.homedir(), '.sf-mcp');
const CACHE_FILE = path.join(CACHE_DIR, 'command-cache.json');
const CACHE_MAX_AGE = 86400 * 7 * 1000; // 1 week in milliseconds
/**
* Clear the command cache
*/
export function clearCommandCache(): boolean {
try {
if (fs.existsSync(CACHE_FILE)) {
fs.unlinkSync(CACHE_FILE);
console.error(`Removed cache file: ${CACHE_FILE}`);
return true;
} else {
console.error(`Cache file does not exist: ${CACHE_FILE}`);
return false;
}
} catch (error) {
console.error('Error clearing command cache:', error);
return false;
}
}
/**
* Manually force the cache to refresh
*/
export function refreshCommandCache(): boolean {
try {
// Clear existing cache
if (fs.existsSync(CACHE_FILE)) {
fs.unlinkSync(CACHE_FILE);
}
// Create a fresh cache
console.error('Refreshing SF command cache...');
// Get all commands directly from sf commands --json
const commands = getAllSfCommands();
console.error(`Found ${commands.length} total commands for cache refresh`);
// Save the cache
saveCommandCache(commands);
console.error('Cache refresh complete!');
return true;
} catch (error) {
console.error('Error refreshing command cache:', error);
return false;
}
}
// Get the full path to the sf command
const SF_BINARY_PATH = (() => {
try {
// Try to find the sf binary in common locations
const possiblePaths = [
'/Users/kpoorman/.volta/bin/sf', // The path we found earlier
'/usr/local/bin/sf',
'/usr/bin/sf',
'/opt/homebrew/bin/sf',
process.env.HOME + '/.npm/bin/sf',
process.env.HOME + '/bin/sf',
process.env.HOME + '/.nvm/versions/node/*/bin/sf',
];
for (const path of possiblePaths) {
try {
if (
execSync(`[ -x "${path}" ] && echo "exists"`, {
encoding: 'utf8',
}).trim() === 'exists'
) {
return path;
}
} catch (e) {
// Path doesn't exist or isn't executable, try the next one
}
}
// If we didn't find it in a known location, try to get it from the PATH
return 'sf';
} catch (e) {
console.error("Unable to locate sf binary, falling back to 'sf'");
return 'sf';
}
})();
/**
* Execute an sf command and return the results
* @param command The sf command to run
* @returns The stdout output from the command
*/
// Store the user-provided project directories (roots)
interface ProjectRoot {
path: string;
name?: string;
description?: string;
isDefault?: boolean;
}
const projectRoots: ProjectRoot[] = [];
let defaultRootPath: string | null = null;
/**
* Validate a directory is a valid Salesforce project
* @param directory The directory to validate
* @returns boolean indicating if valid
*/
function isValidSalesforceProject(directory: string): boolean {
const projectFilePath = path.join(directory, 'sfdx-project.json');
return fs.existsSync(directory) && fs.existsSync(projectFilePath);
}
/**
* Get all configured project roots
* @returns Array of project roots
*/
export function getProjectRoots(): ProjectRoot[] {
return [...projectRoots];
}
/**
* Get the default project directory (for backward compatibility)
* @returns The default project directory or null if none set
*/
export function getDefaultProjectDirectory(): string | null {
return defaultRootPath;
}
/**
* Set the Salesforce project directory to use for commands
* @param directory The directory containing sfdx-project.json
* @param options Optional parameters (name, description, isDefault)
* @returns boolean indicating success
*/
export function setProjectDirectory(
directory: string,
options: { name?: string; description?: string; isDefault?: boolean } = {}
): boolean {
try {
// Validate that the directory exists and contains an sfdx-project.json file
if (!isValidSalesforceProject(directory)) {
console.error(`Invalid Salesforce project: ${directory}`);
return false;
}
// Check if this root already exists
const existingIndex = projectRoots.findIndex(root => root.path === directory);
if (existingIndex >= 0) {
// Update existing root with new options
projectRoots[existingIndex] = {
...projectRoots[existingIndex],
...options,
path: directory
};
// If this is now the default root, update defaultRootPath
if (options.isDefault) {
// Remove default flag from other roots
projectRoots.forEach((root, idx) => {
if (idx !== existingIndex) {
root.isDefault = false;
}
});
defaultRootPath = directory;
}
console.error(`Updated Salesforce project root: ${directory}`);
} else {
// Add as new root
const isDefault = options.isDefault ?? (projectRoots.length === 0);
projectRoots.push({
path: directory,
name: options.name || path.basename(directory),
description: options.description,
isDefault
});
// If this is now the default root, update defaultRootPath
if (isDefault) {
// Remove default flag from other roots
projectRoots.forEach((root, idx) => {
if (idx !== projectRoots.length - 1) {
root.isDefault = false;
}
});
defaultRootPath = directory;
}
console.error(`Added Salesforce project root: ${directory}`);
}
// Always ensure we have exactly one default root if any roots exist
if (projectRoots.length > 0 && !projectRoots.some(root => root.isDefault)) {
projectRoots[0].isDefault = true;
defaultRootPath = projectRoots[0].path;
}
return true;
} catch (error) {
console.error('Error setting project directory:', error);
return false;
}
}
/**
* Checks if a command requires a Salesforce project context
* @param command The SF command to check
* @returns True if the command requires a Salesforce project context
*/
function requiresSalesforceProjectContext(command: string): boolean {
// List of commands or command prefixes that require a Salesforce project context
const projectContextCommands = [
'project deploy',
'project retrieve',
'project delete',
'project convert',
'package version create',
'package1 version create',
'source',
'mdapi',
'apex',
'lightning',
'schema generate'
];
// Check if the command matches any of the project context commands
return projectContextCommands.some(contextCmd => command.startsWith(contextCmd));
}
/**
* Execute an sf command and return the results
* @param command The sf command to run
* @param rootName Optional specific root name to use for execution
* @returns The stdout output from the command
*/
export function executeSfCommand(command: string, rootName?: string): string {
try {
console.error(`Executing: ${SF_BINARY_PATH} ${command}`);
// Check if target-org parameter is 'default' and replace with the default org
if (command.includes('--target-org default') || command.includes('--target-org=default')) {
// Get the default org from sf org list
const orgListOutput = execSync(`"${SF_BINARY_PATH}" org list --json`, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
const orgList = JSON.parse(orgListOutput);
let defaultUsername = '';
// Look for the default org across different org types
for (const orgType of ['nonScratchOrgs', 'scratchOrgs', 'sandboxes']) {
if (orgList.result[orgType]) {
const defaultOrg = orgList.result[orgType].find((org: any) => org.isDefaultUsername);
if (defaultOrg) {
defaultUsername = defaultOrg.username;
break;
}
}
}
if (defaultUsername) {
// Replace 'default' with the actual default org username
command = command.replace(/--target-org[= ]default/, `--target-org ${defaultUsername}`);
console.error(`Using default org: ${defaultUsername}`);
}
}
// Determine which project directory to use
let projectDir: string | null = null;
// If rootName specified, find that specific root
if (rootName) {
const root = projectRoots.find(r => r.name === rootName);
if (root) {
projectDir = root.path;
console.error(`Using specified root "${rootName}" at ${projectDir}`);
} else {
console.error(`Root "${rootName}" not found, falling back to default root`);
// Fall back to default
projectDir = defaultRootPath;
}
} else {
// Use default root
projectDir = defaultRootPath;
}
// Check if this command requires a Salesforce project context and we don't have a project directory
if (requiresSalesforceProjectContext(command) && !projectDir) {
return `This command requires a Salesforce project context (sfdx-project.json).
Please specify a project directory using the format:
"Execute in <directory_path>" or "Use project in <directory_path>"`;
}
try {
// Always execute in project directory if available
if (projectDir) {
console.error(`Executing command in Salesforce project directory: ${projectDir}`);
// Execute the command within the specified project directory
const result = execSync(`"${SF_BINARY_PATH}" ${command}`, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
env: {
...process.env,
PATH: process.env.PATH,
},
cwd: projectDir,
stdio: ['pipe', 'pipe', 'pipe'] // Capture stderr too
});
console.error('Command execution successful');
return result;
} else {
// Standard execution for when no project directory is set
return execSync(`"${SF_BINARY_PATH}" ${command}`, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
env: {
...process.env,
PATH: process.env.PATH,
},
});
}
} catch (execError: any) {
console.error(`Error executing command: ${execError.message}`);
// Capture both stdout and stderr for better error diagnostics
let errorOutput = '';
if (execError.stdout) {
errorOutput += execError.stdout;
}
if (execError.stderr) {
errorOutput += `\n\nError details: ${execError.stderr}`;
}
if (errorOutput) {
console.error(`Command output: ${errorOutput}`);
return errorOutput;
}
return `Error executing command: ${execError.message}`;
}
} catch (error: any) {
console.error(`Top-level error executing command: ${error.message}`);
// Capture both stdout and stderr
let errorOutput = '';
if (error.stdout) {
errorOutput += error.stdout;
}
if (error.stderr) {
errorOutput += `\n\nError details: ${error.stderr}`;
}
if (errorOutput) {
console.error(`Command output: ${errorOutput}`);
return errorOutput;
}
return `Error executing command: ${error.message}`;
}
}
/**
* Get all Salesforce CLI commands using 'sf commands --json'
*/
function getAllSfCommands(): SfCommand[] {
try {
console.error("Fetching all SF CLI commands via 'sf commands --json'...");
// Execute the command to get all commands in JSON format
const commandsJson = executeSfCommand('commands --json');
const allCommands: SfCommandJsonEntry[] = JSON.parse(commandsJson);
console.error(`Found ${allCommands.length} total commands from 'sf commands --json'`);
// Filter out commands from ignored topics
const filteredCommands = allCommands.filter((cmd) => {
if (!cmd.id) return false;
// For commands with colons (topic:command format), check if the topic should be ignored
if (cmd.id.includes(':')) {
const topic = cmd.id.split(':')[0].toLowerCase();
return !IGNORED_TOPICS.includes(topic);
}
// For standalone commands, check if the command itself should be ignored
return !IGNORED_TOPICS.includes(cmd.id.toLowerCase());
});
console.error(`After filtering ignored topics, ${filteredCommands.length} commands remain`);
// Transform JSON commands to SfCommand format
const sfCommands: SfCommand[] = filteredCommands.map((jsonCmd) => {
// Parse the command structure from its ID
const commandParts = jsonCmd.id.split(':');
const isTopicCommand = commandParts.length > 1;
// For commands like "apex:run", extract name and topic
let commandName = isTopicCommand ? commandParts[commandParts.length - 1] : jsonCmd.id;
let topic = isTopicCommand ? commandParts.slice(0, commandParts.length - 1).join(':') : undefined;
// The full command with spaces instead of colons for execution
const fullCommand = jsonCmd.id.replace(/:/g, ' ');
// Convert flags from JSON format to SfFlag format
const flags: SfFlag[] = Object.entries(jsonCmd.flags || {}).map(([flagName, flagDetails]) => {
return {
name: flagName,
char: flagDetails.char,
description: flagDetails.description || '',
required: !!flagDetails.required,
type: flagDetails.type || 'string',
options: flagDetails.options,
default: flagDetails.default,
};
});
return {
id: jsonCmd.id,
name: commandName,
description: jsonCmd.summary || jsonCmd.description || jsonCmd.id,
fullCommand,
flags,
topic,
};
});
console.error(`Successfully processed ${sfCommands.length} commands`);
return sfCommands;
} catch (error) {
console.error('Error getting SF commands:', error);
return [];
}
}
/**
* Convert an SF command to a schema object for validation
*/
function commandToZodSchema(command: SfCommand): Record<string, z.ZodTypeAny> {
const schemaObj: Record<string, z.ZodTypeAny> = {};
for (const flag of command.flags) {
let flagSchema: z.ZodTypeAny;
// Convert flag type to appropriate Zod schema
switch (flag.type) {
case 'number':
case 'integer':
case 'int':
flagSchema = z.number();
break;
case 'boolean':
case 'flag':
flagSchema = z.boolean();
break;
case 'array':
case 'string[]':
flagSchema = z.array(z.string());
break;
case 'json':
case 'object':
flagSchema = z.union([z.string(), z.record(z.any())]);
break;
case 'file':
case 'directory':
case 'filepath':
case 'path':
case 'email':
case 'url':
case 'date':
case 'datetime':
case 'id':
default:
// For options-based flags, create an enum schema
if (flag.options && flag.options.length > 0) {
flagSchema = z.enum(flag.options as [string, ...string[]]);
} else {
flagSchema = z.string();
}
}
// Add description
if (flag.description) {
flagSchema = flagSchema.describe(flag.description);
}
// Make required or optional based on flag definition
schemaObj[flag.name] = flag.required ? flagSchema : flagSchema.optional();
}
return schemaObj;
}
/**
* Get the SF CLI version to use for cache validation
*/
function getSfVersion(): string {
try {
const versionOutput = executeSfCommand('--version');
const versionMatch = versionOutput.match(/sf\/(\d+\.\d+\.\d+)/);
return versionMatch ? versionMatch[1] : 'unknown';
} catch (error) {
console.error('Error getting SF version:', error);
return 'unknown';
}
}
/**
* Saves the SF command data to cache
*/
function saveCommandCache(commands: SfCommand[]): void {
try {
// Create cache directory if it doesn't exist
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
const sfVersion = getSfVersion();
const cache: SfCommandCache = {
version: sfVersion,
timestamp: Date.now(),
commands,
};
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
console.error(`Command cache saved to ${CACHE_FILE} (SF version: ${sfVersion})`);
} catch (error) {
console.error('Error saving command cache:', error);
}
}
/**
* Loads the SF command data from cache
* Returns null if cache is missing, invalid, or expired
*/
function loadCommandCache(): SfCommand[] | null {
try {
if (!fs.existsSync(CACHE_FILE)) {
console.error('Command cache file does not exist');
return null;
}
const cacheData = fs.readFileSync(CACHE_FILE, 'utf8');
const cache = JSON.parse(cacheData) as SfCommandCache;
// Validate cache structure
if (!cache.version || !cache.timestamp || !Array.isArray(cache.commands)) {
console.error('Invalid cache structure');
return null;
}
// Check if cache is expired
const now = Date.now();
if (now - cache.timestamp > CACHE_MAX_AGE) {
console.error('Cache is expired');
return null;
}
// Verify that SF version matches
const currentVersion = getSfVersion();
if (cache.version !== currentVersion) {
console.error(`Cache version mismatch. Cache: ${cache.version}, Current: ${currentVersion}`);
return null;
}
console.error(
`Using command cache from ${new Date(cache.timestamp).toLocaleString()} (SF version: ${cache.version})`
);
console.error(`Found ${cache.commands.length} commands in cache`);
return cache.commands;
} catch (error) {
console.error('Error loading command cache:', error);
return null;
}
}
/**
* Register all SF commands as MCP tools
* @returns The total number of registered tools
*/
export async function registerSfCommands(server: McpServer): Promise<number> {
try {
console.error('Starting SF command registration');
// Try to load commands from cache first
let sfCommands = loadCommandCache();
// If cache doesn't exist or is invalid, fetch commands directly
if (!sfCommands) {
console.error('Cache not available or invalid, fetching commands directly');
sfCommands = getAllSfCommands();
// Save to cache for future use
saveCommandCache(sfCommands);
}
// List of manually defined tools to avoid conflicts
// Only includes the utility cache management tools
const reservedTools = ['sf_cache_clear', 'sf_cache_refresh'];
// Keep track of registered tools and aliases to avoid duplicates
const registeredTools = new Set<string>(reservedTools);
const registeredAliases = new Set<string>();
// Register all commands as tools
let toolCount = 0;
for (const command of sfCommands) {
try {
// Create appropriate MCP-valid tool name
let toolName: string;
if (command.topic) {
// For commands with topics, format as "sf_topic_command"
toolName = `sf_${command.topic.replace(/:/g, '_')}_${command.name}`.replace(/[^a-zA-Z0-9_-]/g, '_');
} else {
// Standalone commands - sf_command
toolName = `sf_${command.name}`.replace(/[^a-zA-Z0-9_-]/g, '_');
}
// Ensure tool name meets length requirements (1-64 characters)
if (toolName.length > 64) {
toolName = toolName.substring(0, 64);
}
// Skip if this tool name conflicts with a manually defined tool or is already registered
if (registeredTools.has(toolName)) {
console.error(`Skipping ${toolName} because it's already registered`);
continue;
}
const zodSchema = commandToZodSchema(command);
// Register the command as a tool with description
server.tool(toolName, command.description, zodSchema, async (flags) => {
const flagsStr = formatFlags(flags);
const commandStr = `${command.fullCommand} ${flagsStr}`;
console.error(`Executing: sf ${commandStr}`);
try {
const output = executeSfCommand(commandStr);
// Check if the output indicates an error but was returned as normal output
if (output && (output.includes('Error executing command') || output.includes('Error details:'))) {
console.error(`Command returned error: ${output}`);
return {
content: [
{
type: 'text',
text: output,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: output,
},
],
};
} catch (error: any) {
console.error(`Error executing ${commandStr}:`, error);
const errorMessage = error.stdout || error.stderr || error.message || 'Unknown error';
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Add to registered tools set and increment counter
registeredTools.add(toolName);
toolCount++;
// For nested commands, create simplified aliases when possible
// (e.g., sf_get for sf_apex_log_get)
if (command.topic && command.topic.includes(':') && command.name.length > 2) {
const simplifiedName = command.name.toLowerCase();
const simplifiedToolName = `sf_${simplifiedName}`.replace(/[^a-zA-Z0-9_-]/g, '_');
// Skip if the simplified name is already registered as a tool or alias
if (registeredTools.has(simplifiedToolName) || registeredAliases.has(simplifiedToolName)) {
continue;
}
// Register simplified alias with description
try {
server.tool(simplifiedToolName, `Alias for ${command.description}`, zodSchema, async (flags) => {
const flagsStr = formatFlags(flags);
const commandStr = `${command.fullCommand} ${flagsStr}`;
console.error(`Executing (via alias ${simplifiedToolName}): sf ${commandStr}`);
try {
const output = executeSfCommand(commandStr);
// Check if the output indicates an error but was returned as normal output
if (output && (output.includes('Error executing command') || output.includes('Error details:'))) {
console.error(`Command returned error: ${output}`);
return {
content: [
{
type: 'text',
text: output,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: output,
},
],
};
} catch (error: any) {
console.error(`Error executing ${commandStr}:`, error);
const errorMessage = error.stdout || error.stderr || error.message || 'Unknown error';
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Add alias to tracking sets and increment counter
registeredAliases.add(simplifiedToolName);
registeredTools.add(simplifiedToolName);
toolCount++;
console.error(`Registered alias ${simplifiedToolName} for ${toolName}`);
} catch (err) {
console.error(`Error registering alias ${simplifiedToolName}:`, err);
}
}
} catch (err) {
console.error(`Error registering tool for command ${command.id}:`, err);
}
}
const totalTools = toolCount + registeredAliases.size;
console.error(
`Registration complete. Registered ${totalTools} tools (${toolCount} commands and ${registeredAliases.size} aliases).`
);
// Return the count for the main server to use
return totalTools;
} catch (error) {
console.error('Error registering SF commands:', error);
return 0;
}
}
```