#
tokens: 42464/50000 26/26 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | {
2 |   "semi": true,
3 |   "trailingComma": "es5",
4 |   "singleQuote": true,
5 |   "printWidth": 120,
6 |   "tabWidth": 4,
7 |   "useTabs": false,
8 |   "endOfLine":"lf"
9 | }
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | ### Node template
  2 | # Logs
  3 | logs
  4 | *.log
  5 | npm-debug.log*
  6 | yarn-debug.log*
  7 | yarn-error.log*
  8 | lerna-debug.log*
  9 | .pnpm-debug.log*
 10 | 
 11 | # Diagnostic reports (https://nodejs.org/api/report.html)
 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 13 | 
 14 | # Runtime data
 15 | pids
 16 | *.pid
 17 | *.seed
 18 | *.pid.lock
 19 | 
 20 | # Directory for instrumented libs generated by jscoverage/JSCover
 21 | lib-cov
 22 | 
 23 | # Coverage directory used by tools like istanbul
 24 | coverage
 25 | *.lcov
 26 | 
 27 | # nyc test coverage
 28 | .nyc_output
 29 | 
 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 31 | .grunt
 32 | 
 33 | # Bower dependency directory (https://bower.io/)
 34 | bower_components
 35 | 
 36 | # node-waf configuration
 37 | .lock-wscript
 38 | 
 39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 40 | build/Release
 41 | 
 42 | # Dependency directories
 43 | node_modules/
 44 | jspm_packages/
 45 | 
 46 | # Snowpack dependency directory (https://snowpack.dev/)
 47 | web_modules/
 48 | 
 49 | # TypeScript cache
 50 | *.tsbuildinfo
 51 | 
 52 | # Optional npm cache directory
 53 | .npm
 54 | 
 55 | # Optional eslint cache
 56 | .eslintcache
 57 | 
 58 | # Optional stylelint cache
 59 | .stylelintcache
 60 | 
 61 | # Microbundle cache
 62 | .rpt2_cache/
 63 | .rts2_cache_cjs/
 64 | .rts2_cache_es/
 65 | .rts2_cache_umd/
 66 | 
 67 | # Optional REPL history
 68 | .node_repl_history
 69 | 
 70 | # Output of 'npm pack'
 71 | *.tgz
 72 | 
 73 | # Yarn Integrity file
 74 | .yarn-integrity
 75 | 
 76 | # dotenv environment variable files
 77 | .env
 78 | .env.development.local
 79 | .env.test.local
 80 | .env.production.local
 81 | .env.local
 82 | 
 83 | # parcel-bundler cache (https://parceljs.org/)
 84 | .cache
 85 | .parcel-cache
 86 | 
 87 | # Next.js build output
 88 | .next
 89 | out
 90 | 
 91 | # Nuxt.js build / generate output
 92 | .nuxt
 93 | dist
 94 | 
 95 | # Gatsby files
 96 | .cache/
 97 | # Comment in the public line in if your project uses Gatsby and not Next.js
 98 | # https://nextjs.org/blog/next-9-1#public-directory-support
 99 | # public
100 | 
101 | # vuepress build output
102 | .vuepress/dist
103 | 
104 | # vuepress v2.x temp and cache directory
105 | .temp
106 | .cache
107 | 
108 | # Docusaurus cache and generated files
109 | .docusaurus
110 | 
111 | # Serverless directories
112 | .serverless/
113 | 
114 | # FuseBox cache
115 | .fusebox/
116 | 
117 | # DynamoDB Local files
118 | .dynamodb/
119 | 
120 | # TernJS port file
121 | .tern-port
122 | 
123 | # Stores VSCode versions used for testing VSCode extensions
124 | .vscode-test
125 | 
126 | # yarn v2
127 | .yarn/cache
128 | .yarn/unplugged
129 | .yarn/build-state.yml
130 | .yarn/install-state.gz
131 | .pnp.*
132 | 
133 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Salesforce CLI MCP Server
  2 | 
  3 | Model Context Protocol (MCP) server for providing Salesforce CLI functionality to LLM tools like Claude Desktop.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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:
  8 | 
  9 | - View help information about Salesforce CLI topics and commands
 10 | - Execute Salesforce CLI commands with appropriate parameters
 11 | - Leverage Salesforce CLI capabilities in AI workflows
 12 | 
 13 | ## Requirements
 14 | 
 15 | - Node.js 18+ and npm
 16 | - Salesforce CLI (`sf`) installed and configured
 17 | - Your Salesforce org credentials configured in the CLI
 18 | 
 19 | ## Installation
 20 | 
 21 | ```bash
 22 | # Clone the repository
 23 | git clone <repository-url>
 24 | cd sfMcp
 25 | 
 26 | # Install dependencies
 27 | npm install
 28 | ```
 29 | 
 30 | ## Usage
 31 | 
 32 | ### Starting the server
 33 | 
 34 | ```bash
 35 | # Basic usage
 36 | npm start
 37 | 
 38 | # With project roots
 39 | npm start /path/to/project1 /path/to/project2
 40 | # or using the convenience script
 41 | npm run with-roots /path/to/project1 /path/to/project2
 42 | 
 43 | # As an npx package with roots
 44 | npx -y codefriar/sf-mcp /path/to/project1 /path/to/project2
 45 | ```
 46 | 
 47 | The MCP server uses stdio transport, which can be used with MCP clients 
 48 | like the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or Claude Desktop.
 49 | 
 50 | ### Configuring in Claude Desktop
 51 | 
 52 | To configure this MCP in Claude Desktop's `.claude.json` configuration:
 53 | 
 54 | ```json
 55 | {
 56 |   "tools": {
 57 |     "salesforce": {
 58 |       "command": "/path/to/node",
 59 |       "args": [
 60 |         "/path/to/sf-mcp/build/index.js",
 61 |         "/path/to/project1",
 62 |         "/path/to/project2"
 63 |       ]
 64 |     }
 65 |   }
 66 | }
 67 | ```
 68 | 
 69 | Using the npm package directly:
 70 | 
 71 | ```json
 72 | {
 73 |   "tools": {
 74 |     "salesforce": {
 75 |       "command": "/path/to/npx", 
 76 |       "args": [
 77 |         "-y",
 78 |         "codefriar/sf-mcp",
 79 |         "/path/to/project1",
 80 |         "/path/to/project2"
 81 |       ]
 82 |     }
 83 |   }
 84 | }
 85 | ```
 86 | 
 87 | ### Development
 88 | 
 89 | ```bash
 90 | # Watch mode (recompiles on file changes)
 91 | npm run dev
 92 | 
 93 | # In another terminal
 94 | npm start [optional project roots...]
 95 | ```
 96 | 
 97 | ## Available Tools and Resources
 98 | 
 99 | 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.
100 | 
101 | ### Core Tools
102 | 
103 | - `sf_version` - Get the Salesforce CLI version information
104 | - `sf_help` - Get help information for Salesforce CLI commands
105 | - `sf_cache_clear` - Clear the command discovery cache
106 | - `sf_cache_refresh` - Refresh the command discovery cache
107 | 
108 | ### Project Directory Management (Roots)
109 | 
110 | For commands that require a Salesforce project context (like deployments), you must specify the project directory.
111 | The MCP supports multiple project directories (roots) similar to the filesystem MCP.
112 | 
113 | #### Configuration Methods
114 | 
115 | **Method 1: Via Command Line Arguments**
116 | ```bash
117 | # Start the MCP with project roots
118 | npm start /path/to/project1 /path/to/project2
119 | # or
120 | npx -y codefriar/sf-mcp /path/to/project1 /path/to/project2
121 | ```
122 | 
123 | When configured this way, the roots will be automatically named `root1`, `root2`, etc., 
124 | with the first one set as default.
125 | 
126 | **Method 2: Using MCP Tools**
127 | - `sf_set_project_directory` - Set a Salesforce project directory to use for commands
128 |   - Parameters: 
129 |     - `directory` - Path to a directory containing a sfdx-project.json file
130 |     - `name` - (Optional) Name for this project root
131 |     - `description` - (Optional) Description for this project root
132 |     - `isDefault` - (Optional) Set this root as the default for command execution
133 | - `sf_list_roots` - List all configured project roots
134 | - `sf_detect_project_directory` - Attempt to detect project directory from user messages
135 | 
136 | Example usage:
137 | ```
138 | # Set project directory with a name
139 | sf_set_project_directory --directory=/path/to/your/sfdx/project --name=project1 --isDefault=true
140 | 
141 | # List all configured roots
142 | sf_list_roots
143 | 
144 | # Or include in your message:
145 | "Please deploy the apex code from the project in /path/to/your/sfdx/project to my scratch org"
146 | ```
147 | 
148 | **Method 3: Claude Desktop Configuration**
149 | Configure project roots in `.claude.json` as described below.
150 | 
151 | #### Using Project Roots
152 | 
153 | You can execute commands in specific project roots:
154 | ```
155 | # Using resource URI
156 | sf://roots/project1/commands/project deploy start --sourcedir=force-app
157 | 
158 | # Using rootName parameter
159 | sf_project_deploy_start --sourcedir=force-app --rootName=project1
160 | ```
161 | 
162 | Project directory must be specified for commands such as deployments,
163 | source retrieval, and other project-specific operations. 
164 | If multiple roots are configured, the default root will be used unless otherwise specified.
165 | 
166 | ### Key Implemented Tools
167 | 
168 | The following commands are specifically implemented and guaranteed to work:
169 | 
170 | #### Organization Management
171 | 
172 | - `sf_org_list` - List Salesforce orgs
173 |     - Parameters: `json`, `verbose`
174 | - `sf_auth_list_orgs` - List authenticated Salesforce orgs
175 |     - Parameters: `json`, `verbose`
176 | - `sf_org_display` - Display details about an org
177 |     - Parameters: `targetusername`, `json`
178 | - `sf_org_open` - Open an org in the browser
179 |     - Parameters: `targetusername`, `path`, `urlonly`
180 | 
181 | #### Apex Code
182 | 
183 | - `sf_apex_run` - Run anonymous Apex code
184 |     - Parameters: `targetusername`, `file`, `apexcode`, `json`
185 | - `sf_apex_test_run` - Run Apex tests
186 |     - Parameters: `targetusername`, `testnames`, `suitenames`, `classnames`, `json`
187 | 
188 | #### Data Management
189 | 
190 | - `sf_data_query` - Execute a SOQL query
191 |     - Parameters: `targetusername`, `query`, `json`
192 | - `sf_schema_list_objects` - List sObjects in the org
193 |     - Parameters: `targetusername`, `json`
194 | - `sf_schema_describe` - Describe a Salesforce object
195 |     - Parameters: `targetusername`, `sobject`, `json`
196 | 
197 | #### Deployment
198 | 
199 | - `sf_project_deploy_start` - Deploy the source to an org
200 |     - Parameters: `targetusername`, `sourcedir`, `json`, `wait`
201 | 
202 | ### Dynamically Discovered Tools
203 | 
204 | The server discovers all available Salesforce CLI commands and registers them as tools with format: `sf_<topic>_<command>`.
205 | 
206 | For example:
207 | 
208 | - `sf_apex_run` - Run anonymous Apex code
209 | - `sf_data_query` - Execute a SOQL query
210 | 
211 | For nested topic commands, the tool name includes the full path with underscores:
212 | 
213 | - `sf_apex_log_get` - Get apex logs
214 | - `sf_org_login_web` - Login to an org using web flow
215 | 
216 | The server also creates simplified aliases for common nested commands where possible:
217 | 
218 | - `sf_get` as an alias for `sf_apex_log_get`
219 | - `sf_web` as an alias for `sf_org_login_web`
220 | 
221 | The available commands vary depending on the installed Salesforce CLI plugins.
222 | 
223 | > **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.
224 | 
225 | ### Resources
226 | 
227 | The following resources provide documentation about Salesforce CLI:
228 | 
229 | - `sf://help` - Main CLI documentation
230 | - `sf://topics/{topic}/help` - Topic help documentation
231 | - `sf://commands/{command}/help` - Command help documentation
232 | - `sf://topics/{topic}/commands/{command}/help` - Topic-command help documentation
233 | - `sf://version` - Version information
234 | - `sf://roots` - List all configured project roots
235 | - `sf://roots/{root}/commands/{command}` - Execute a command in a specific project root
236 | 
237 | ## How It Works
238 | 
239 | 1. At startup, the server checks for a cached list of commands (stored in `~/.sf-mcp/command-cache.json`)
240 | 2. If a valid cache exists, it's used to register commands; otherwise, commands are discovered dynamically
241 | 3. During discovery, the server queries `sf commands --json` to get a complete list of available commands
242 | 4. Command metadata (including parameters and descriptions) is extracted directly from the JSON output
243 | 5. All commands are registered as MCP tools with appropriate parameter schemas
244 | 6. Resources are registered for help documentation
245 | 7. When a tool is called, the corresponding Salesforce CLI command is executed
246 | 
247 | ### Project Roots Management
248 | 
249 | For commands that require a Salesforce project context:
250 | 
251 | 1. The server checks if any project roots have been configured via `sf_set_project_directory`
252 | 2. If multiple roots are configured, it uses the default root unless a specific root is specified
253 | 3. If no roots are set, the server will prompt the user to specify a project directory
254 | 4. Commands are executed within the appropriate project directory, ensuring proper context
255 | 5. The user can add or switch between multiple project roots as needed
256 | 
257 | Project-specific commands (like deployments, retrievals, etc.) 
258 | will automatically run in the appropriate project directory. 
259 | For commands that don't require a project context, the working directory doesn't matter.
260 | 
261 | You can execute commands in specific project roots by:
262 | - Using the resource URI: `sf://roots/{rootName}/commands/{command}`
263 | - Providing a `rootName` parameter to command tools (internal implementation details)
264 | - Setting a specific root as the default with `sf_set_project_directory --isDefault=true`
265 | 
266 | ### Command Caching
267 | 
268 | To improve startup performance, the MCP server caches discovered commands:
269 | 
270 | - The cache is stored in `~/.sf-mcp/command-cache.json`
271 | - It includes all topics, commands, parameters, and descriptions
272 | - The cache has a validation timestamp and SF CLI version check
273 | - By default, the cache expires after 7 days
274 | - When you install new Salesforce CLI plugins, use `sf_cache_refresh` to update the cache
275 | 
276 | #### Troubleshooting Cache Issues
277 | 
278 | 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:
279 | 
280 | 1. Stop the MCP server (if running)
281 | 2. Manually delete the cache file: `rm ~/.sf-mcp/command-cache.json`
282 | 3. Start the server again: `npm start`
283 | 
284 | This will force a complete rediscovery of all commands using the official CLI metadata.
285 | 
286 | If specific commands are still missing, or you've installed new SF CLI plugins:
287 | 
288 | 1. Use the `sf_cache_refresh` tool from Claude Desktop
289 | 2. Stop and restart the MCP server
290 | 
291 | ### Handling Nested Topics
292 | 
293 | The Salesforce CLI has a hierarchical command structure that can be several levels deep. This MCP server handles these nested commands by:
294 | 
295 | - Converting colon-separated paths to underscore format (`apex:log:get` → `sf_apex_log_get`)
296 | - Providing aliases for common deep commands when possible (`sf_get` for `sf_apex_log_get`)
297 | - Preserving the full command hierarchy in the tool names
298 | - Using the official command structure from `sf commands --json`
299 | 
300 | Nested topic commands are registered twice when possible—once with the full hierarchy name and once with a simplified alias,
301 | making them easier to discover and use.
302 | 
303 | ## License
304 | 
305 | ISC
306 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP Development Guide
 2 | 
 3 | ## Build Commands
 4 | 
 5 | - Build project: `npm run build`
 6 | - Run the MCP server: `node build/index.js`
 7 | 
 8 | ## Lint & Formatting
 9 | 
10 | - Format with Prettier: `npx prettier --write 'src/**/*.ts'`
11 | - Lint: `npx eslint 'src/**/*.ts'`
12 | - Type check: `npx tsc --noEmit`
13 | 
14 | ## Testing
15 | 
16 | - Run tests: `npm test`
17 | - Run a single test: `npm test -- -t 'test name'`
18 | 
19 | ## Code Style Guidelines
20 | 
21 | - Use ES modules (import/export) syntax
22 | - TypeScript strict mode enabled
23 | - Types: Use strong typing with TypeScript interfaces/types
24 | - Naming: camelCase for variables/functions, PascalCase for classes/interfaces
25 | - Error handling: Use try/catch with typed errors
26 | - Imports: Group by 3rd party, then local, alphabetized within groups
27 | - Async: Prefer async/await over raw Promises
28 | - Documentation: JSDoc for public APIs
29 | - Endpoint API naming following MCP conventions for resources/tools/prompts
30 | 
31 | ## Project Description
32 | 
33 | - 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)
34 | 
35 | ## Model Context Protocol (MCP) Architecture
36 | 
37 | ### Core Components
38 | - **Hosts**: LLM applications (Claude Desktop, Claude Code) that initiate connections
39 | - **Clients**: Maintain connections with MCP servers
40 | - **Servers**: Provide context, tools, and prompts (this Salesforce CLI MCP server)
41 | 
42 | ### MCP Primitives for Salesforce Integration
43 | 
44 | #### Tools
45 | - Executable functions for Salesforce operations
46 | - Dynamic tool discovery and invocation
47 | - Tool annotations (read-only, destructive operations)
48 | - Key Salesforce tools to implement:
49 |   - SOQL query execution
50 |   - Record CRUD operations
51 |   - Metadata deployment/retrieval
52 |   - Org inspection and configuration
53 |   - Apex execution and testing
54 | 
55 | #### Resources
56 | - Expose Salesforce data and metadata
57 | - Unique URI identification for resources
58 | - Support for text and binary content
59 | - Salesforce resources to expose:
60 |   - Object schemas and field definitions
61 |   - Org configuration and limits
62 |   - Deployment metadata
63 |   - Code coverage reports
64 |   - Flow definitions
65 | 
66 | #### Prompts
67 | - Reusable prompt templates for Salesforce workflows
68 | - Dynamic arguments for context-aware interactions
69 | - Common Salesforce prompt patterns:
70 |   - Data analysis and reporting
71 |   - Code generation and review
72 |   - Deployment guidance
73 |   - Best practices recommendations
74 | 
75 | #### Sampling
76 | - Allow server to request LLM completions
77 | - Human-in-the-loop approval for destructive operations
78 | - Fine-grained control over Salesforce operations
79 | 
80 | ### Security Considerations
81 | - Input validation for all Salesforce CLI commands
82 | - Proper authentication with Salesforce orgs
83 | - Rate limiting to respect Salesforce API limits
84 | - Sanitization of external interactions
85 | - Secure handling of sensitive org data
86 | 
87 | ### Transport
88 | - Primary: Stdio (standard input/output)
89 | - Alternative: HTTP with Server-Sent Events (SSE)
90 | 
91 | ### Implementation Strategy
92 | 1. Start with core Salesforce CLI tools (query, describe, deploy)
93 | 2. Use TypeScript MCP SDK for type safety
94 | 3. Implement robust error handling for CLI failures
95 | 4. Provide clear tool descriptions and examples
96 | 5. Add progressive enhancement for advanced features
```

--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------

```
1 | <component name="ProjectCodeStyleConfiguration">
2 |   <state>
3 |     <option name="USE_PER_PROJECT_SETTINGS" value="true" />
4 |   </state>
5 | </component>
```

--------------------------------------------------------------------------------
/.idea/git_toolbox_blame.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="GitToolBoxBlameSettings">
4 |     <option name="version" value="2" />
5 |   </component>
6 | </project>
```

--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="PrettierConfiguration">
4 |     <option name="myConfigurationMode" value="AUTOMATIC" />
5 |     <option name="myRunOnSave" value="true" />
6 |   </component>
7 | </project>
```

--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------

```
1 | <component name="InspectionProjectProfileManager">
2 |   <profile version="1.0">
3 |     <option name="myName" value="Project Default" />
4 |     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5 |   </profile>
6 | </component>
```

--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="ProjectModuleManager">
4 |     <modules>
5 |       <module fileurl="file://$PROJECT_DIR$/.idea/sf-mcp.iml" filepath="$PROJECT_DIR$/.idea/sf-mcp.iml" />
6 |     </modules>
7 |   </component>
8 | </project>
```

--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------

```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="EslintConfiguration">
4 |     <work-dir-patterns value="$PROJECT_DIR$" />
5 |     <custom-configuration-file used="true" path="$PROJECT_DIR$/eslint.config.js" />
6 |     <option name="fix-on-save" value="true" />
7 |   </component>
8 | </project>
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |         "target": "ES2022",
 4 |         "module": "Node16",
 5 |         "moduleResolution": "Node16",
 6 |         "outDir": "./build",
 7 |         "rootDir": "./src",
 8 |         "strict": true,
 9 |         "esModuleInterop": true,
10 |         "skipLibCheck": true,
11 |         "forceConsistentCasingInFileNames": true
12 |     },
13 |     "include": ["src/**/*"],
14 |     "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/.idea/shelf/Uncommitted_changes_before_Checkout_at_4_8_25__22_47__Changes_.xml:
--------------------------------------------------------------------------------

```
1 | <changelist name="Uncommitted_changes_before_Checkout_at_4_8_25,_22_47_[Changes]" date="1744177621660" recycled="true" deleted="true">
2 |   <option name="PATH" value="$PROJECT_DIR$/.idea/shelf/Uncommitted_changes_before_Checkout_at_4_8_25,_22_47_[Changes]/shelved.patch" />
3 |   <option name="DESCRIPTION" value="Uncommitted changes before Checkout at 4/8/25, 22:47 [Changes]" />
4 | </changelist>
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | // @ts-check
 2 | 
 3 | import eslint from '@eslint/js';
 4 | import tseslint from 'typescript-eslint';
 5 | 
 6 | export default tseslint.config(
 7 |     eslint.configs.recommended,
 8 |     tseslint.configs.recommendedTypeChecked,
 9 |     // tseslint.configs.strictTypeChecked,
10 |     // tseslint.configs.stylisticTypeChecked,
11 |     {
12 |         languageOptions: {
13 |             parserOptions: {
14 |                 projectService: true,
15 |                 tsconfigRootDir: import.meta.dirname,
16 |             },
17 |         },
18 |     },
19 | );
```

--------------------------------------------------------------------------------
/.idea/git_toolbox_prj.xml:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <project version="4">
 3 |   <component name="GitToolBoxProjectSettings">
 4 |     <option name="commitMessageIssueKeyValidationOverride">
 5 |       <BoolValueOverride>
 6 |         <option name="enabled" value="true" />
 7 |       </BoolValueOverride>
 8 |     </option>
 9 |     <option name="commitMessageValidationEnabledOverride">
10 |       <BoolValueOverride>
11 |         <option name="enabled" value="true" />
12 |       </BoolValueOverride>
13 |     </option>
14 |   </component>
15 | </project>
```

--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <project version="4">
 3 |   <component name="CommitMessageInspectionProfile">
 4 |     <profile version="1.0">
 5 |       <inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
 6 |       <inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
 7 |     </profile>
 8 |   </component>
 9 |   <component name="VcsDirectoryMappings">
10 |     <mapping directory="" vcs="Git" />
11 |   </component>
12 | </project>
```

--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Check if sf command is available
 4 | if ! command -v sf &> /dev/null; then
 5 |     echo "Error: Salesforce CLI (sf) is not installed or not in your PATH"
 6 |     echo "Please install it from: https://developer.salesforce.com/tools/sfdxcli"
 7 |     exit 1
 8 | fi
 9 | 
10 | # Print current directory and sf version
11 | echo "Current directory: $(pwd)"
12 | echo "Salesforce CLI version:"
13 | sf --version
14 | 
15 | # Build and run the server
16 | echo "Building and starting MCP server..."
17 | npm run build
18 | 
19 | # Pass all command-line arguments to the server (for project roots)
20 | node build/index.js "$@"
```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
 3 | 
 4 | name: Node.js Package
 5 | 
 6 | on:
 7 |   release:
 8 |     types: [created]
 9 | 
10 | jobs:
11 |   build:
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |       - uses: actions/checkout@v4
15 |       - uses: actions/setup-node@v4
16 |         with:
17 |           node-version: 20
18 |       - run: npm ci
19 |       - run: npm test
20 | 
21 |   publish-npm:
22 |     needs: build
23 |     runs-on: ubuntu-latest
24 |     steps:
25 |       - uses: actions/checkout@v4
26 |       - uses: actions/setup-node@v4
27 |         with:
28 |           node-version: 20
29 |           registry-url: https://registry.npmjs.org/
30 |       - run: npm ci
31 |       - run: npm publish
32 |         env:
33 |           NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
34 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "name": "sf-mcp",
 3 |     "version": "1.3.2",
 4 |     "main": "build/index.js",
 5 |     "type": "module",
 6 |     "bin": {
 7 |         "sfmcp": "./build/index.js"
 8 |     },
 9 |     "scripts": {
10 |         "build": "tsc && chmod 755 build/index.js",
11 |         "start": "node build/index.js",
12 |         "dev": "tsc -w",
13 |         "lint": "eslint src",
14 |         "prepare": "npm run build",
15 |         "format": "prettier --write \"**/*.{ts,json,md}\"",
16 |         "test": "echo \"No tests configured\" && exit 0",
17 |         "release": "standard-version && git push --follow-tags origin main && npm publish",
18 |         "with-roots": "./run.sh"
19 |     },
20 |     "files": [
21 |         "build",
22 |         "run.sh"
23 |     ],
24 |     "keywords": [
25 |         "mcp",
26 |         "modelcontextprotocol",
27 |         "salesforce",
28 |         "sf",
29 |         "cli",
30 |         "llm"
31 |     ],
32 |     "author": "Kevin Poorman",
33 |     "license": "ISC",
34 |     "description": "Model Context Protocol (MCP) server for the Salesforce CLI, making Salesforce CLI commands available to LLM tools like Claude Desktop.",
35 |     "repository": {
36 |         "type": "git",
37 |         "url": "https://github.com/codefriar/sf-mcp"
38 |     },
39 |     "dependencies": {
40 |         "@modelcontextprotocol/sdk": "^1.8.0",
41 |         "zod": "^3.24.2"
42 |     },
43 |     "devDependencies": {
44 |         "@eslint/js": "^9.23.0",
45 |         "@types/node": "^22.13.14",
46 |         "eslint": "^9.23.0",
47 |         "prettier": "^3.5.3",
48 |         "standard-release": "^0.2.0",
49 |         "standard-version": "^9.5.0",
50 |         "typescript": "^5.8.2",
51 |         "typescript-eslint": "^8.28.0"
52 |     }
53 | }
54 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | 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.
 4 | 
 5 | ### [1.3.2](https://github.com/codefriar/sf-mcp/compare/v1.3.1...v1.3.2) (2025-05-27)
 6 | 
 7 | ### [1.3.1](https://github.com/codefriar/sf-mcp/compare/v1.3.0...v1.3.1) (2025-04-09)
 8 | 
 9 | ## [1.3.0](https://github.com/codefriar/sf-mcp/compare/v1.1.1...v1.3.0) (2025-04-09)
10 | 
11 | 
12 | ### Features
13 | 
14 | * add contextual execution with project directory detection ([0c02e01](https://github.com/codefriar/sf-mcp/commit/0c02e0100da6906ea0ece9e26e0fd75ec0886044))
15 | * add Salesforce project directory handling for contextual command execution ([5c1ddc3](https://github.com/codefriar/sf-mcp/commit/5c1ddc3783a0e8e80f357dfb0c9c082e8710b36d))
16 | * **context directories:** All commands require a directory ([4b1d76b](https://github.com/codefriar/sf-mcp/commit/4b1d76b0b38c9b5b01b12efbed2ad107320af3c2))
17 | * **roots:** Now with Roots ([da38db3](https://github.com/codefriar/sf-mcp/commit/da38db3187809b42c47604d9d078238d2d02705a))
18 | 
19 | ## [1.2.0](https://github.com/codefriar/sf-mcp/compare/v1.1.1...v1.2.0) (2025-04-09)
20 | 
21 | 
22 | ### Features
23 | 
24 | * add contextual execution with project directory detection ([0c02e01](https://github.com/codefriar/sf-mcp/commit/0c02e0100da6906ea0ece9e26e0fd75ec0886044))
25 | * add Salesforce project directory handling for contextual command execution ([5c1ddc3](https://github.com/codefriar/sf-mcp/commit/5c1ddc3783a0e8e80f357dfb0c9c082e8710b36d))
26 | * **context directories:** All commands require a directory ([4b1d76b](https://github.com/codefriar/sf-mcp/commit/4b1d76b0b38c9b5b01b12efbed2ad107320af3c2))
27 | * **roots:** Now with Roots ([da38db3](https://github.com/codefriar/sf-mcp/commit/da38db3187809b42c47604d9d078238d2d02705a))
28 | 
29 | ### [1.1.1](https://github.com/codefriar/sf-mcp/compare/v1.1.0...v1.1.1) (2025-04-02)
30 | 
31 | ## 1.1.0 (2025-03-28)
32 | 
33 | 
34 | ### Features
35 | 
36 | * **tools:** autodiscovery of tools ([532c685](https://github.com/codefriar/sf-mcp/commit/532c685aa8b22f01e81b4bfa69024c14a05d932d))
37 | 
```

--------------------------------------------------------------------------------
/src/resources.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { executeSfCommand, getProjectRoots } from './sfCommands.js';
  3 | 
  4 | /**
  5 |  * Register all resources for the SF CLI MCP Server
  6 |  */
  7 | export function registerResources(server: McpServer): void {
  8 |     // Main CLI documentation
  9 |     server.resource('sf-help', 'sf://help', async (uri) => ({
 10 |         contents: [
 11 |             {
 12 |                 uri: uri.href,
 13 |                 text: executeSfCommand('-h'),
 14 |             },
 15 |         ],
 16 |     }));
 17 | 
 18 |     // Project roots information
 19 |     server.resource('sf-roots', 'sf://roots', async (uri) => {
 20 |         const roots = getProjectRoots();
 21 |         const rootsText = roots.length > 0 
 22 |             ? roots.map(root => `${root.name}${root.isDefault ? ' (default)' : ''}: ${root.path}${root.description ? ` - ${root.description}` : ''}`).join('\n')
 23 |             : 'No project roots configured. Use sf_set_project_directory to add a project root.';
 24 |         
 25 |         return {
 26 |             contents: [
 27 |                 {
 28 |                     uri: uri.href,
 29 |                     text: rootsText,
 30 |                 },
 31 |             ],
 32 |         };
 33 |     });
 34 | 
 35 |     // Topic help documentation
 36 |     server.resource(
 37 |         'sf-topic-help',
 38 |         new ResourceTemplate('sf://topics/{topic}/help', { list: undefined }),
 39 |         async (uri, { topic }) => ({
 40 |             contents: [
 41 |                 {
 42 |                     uri: uri.href,
 43 |                     text: executeSfCommand(`${topic} -h`),
 44 |                 },
 45 |             ],
 46 |         })
 47 |     );
 48 | 
 49 |     // Command help documentation
 50 |     server.resource(
 51 |         'sf-command-help',
 52 |         new ResourceTemplate('sf://commands/{command}/help', { list: undefined }),
 53 |         async (uri, { command }) => ({
 54 |             contents: [
 55 |                 {
 56 |                     uri: uri.href,
 57 |                     text: executeSfCommand(`${command} -h`),
 58 |                 },
 59 |             ],
 60 |         })
 61 |     );
 62 | 
 63 |     // Topic-command help documentation
 64 |     server.resource(
 65 |         'sf-topic-command-help',
 66 |         new ResourceTemplate('sf://topics/{topic}/commands/{command}/help', {
 67 |             list: undefined,
 68 |         }),
 69 |         async (uri, { topic, command }) => ({
 70 |             contents: [
 71 |                 {
 72 |                     uri: uri.href,
 73 |                     text: executeSfCommand(`${topic} ${command} -h`),
 74 |                 },
 75 |             ],
 76 |         })
 77 |     );
 78 | 
 79 |     // Root-specific command help (execute in a specific root)
 80 |     server.resource(
 81 |         'sf-root-command',
 82 |         new ResourceTemplate('sf://roots/{root}/commands/{command}', { list: undefined }),
 83 |         async (uri, { root, command }) => ({
 84 |             contents: [
 85 |                 {
 86 |                     uri: uri.href,
 87 |                     // Ensure command is treated as string
 88 |                     text: executeSfCommand(String(command), String(root)),
 89 |                 },
 90 |             ],
 91 |         })
 92 |     );
 93 | 
 94 |     // Version information
 95 |     server.resource('sf-version', 'sf://version', async (uri) => ({
 96 |         contents: [
 97 |             {
 98 |                 uri: uri.href,
 99 |                 text: executeSfCommand('--version'),
100 |             },
101 |         ],
102 |     }));
103 | }
104 | 
```

--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------

```
 1 | <component name="ProjectCodeStyleConfiguration">
 2 |   <code_scheme name="Project" version="173">
 3 |     <option name="LINE_SEPARATOR" value="&#10;" />
 4 |     <ApexCodeStyleSettings>
 5 |       <option name="ALIGN_MULTILINE_COMPOSITE_EXPRESSIONS" value="true" />
 6 |       <option name="USAGE_CASE_APEX_CHANGE_CASE_WHEN_FORMATTING" value="true" />
 7 |       <option name="KEYWORD_CASE_APEX_CHANGE_CASE_WHEN_FORMATTING" value="true" />
 8 |       <option name="KEYWORD_CASE_SOQL_SOSL_CHANGE_CASE_WHEN_FORMATTING" value="true" />
 9 |       <option name="KEYWORD_CASE_VISUALFORCE_CHANGE_CASE_WHEN_FORMATTING" value="true" />
10 |       <option name="KEYWORD_CASE_AURA_CHANGE_CASE_WHEN_FORMATTING" value="true" />
11 |       <option name="APEX_DOC_REFORMAT_APEX_DOC" value="true" />
12 |       <option name="APEX_DOC_REQUIRE_DESCRIPTION_TAG" value="true" />
13 |     </ApexCodeStyleSettings>
14 |     <HTMLCodeStyleSettings>
15 |       <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
16 |     </HTMLCodeStyleSettings>
17 |     <JSCodeStyleSettings version="0">
18 |       <option name="FORCE_SEMICOLON_STYLE" value="true" />
19 |       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
20 |       <option name="USE_DOUBLE_QUOTES" value="false" />
21 |       <option name="FORCE_QUOTE_STYlE" value="true" />
22 |       <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
23 |       <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
24 |       <option name="SPACES_WITHIN_IMPORTS" value="true" />
25 |     </JSCodeStyleSettings>
26 |     <TypeScriptCodeStyleSettings version="0">
27 |       <option name="FORCE_SEMICOLON_STYLE" value="true" />
28 |       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
29 |       <option name="USE_DOUBLE_QUOTES" value="false" />
30 |       <option name="FORCE_QUOTE_STYlE" value="true" />
31 |       <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
32 |       <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
33 |       <option name="SPACES_WITHIN_IMPORTS" value="true" />
34 |     </TypeScriptCodeStyleSettings>
35 |     <VueCodeStyleSettings>
36 |       <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
37 |       <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
38 |     </VueCodeStyleSettings>
39 |     <codeStyleSettings language="Apex">
40 |       <indentOptions>
41 |         <option name="USE_TAB_CHARACTER" value="true" />
42 |         <option name="SMART_TABS" value="true" />
43 |         <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
44 |       </indentOptions>
45 |     </codeStyleSettings>
46 |     <codeStyleSettings language="HTML">
47 |       <option name="SOFT_MARGINS" value="120" />
48 |       <indentOptions>
49 |         <option name="CONTINUATION_INDENT_SIZE" value="4" />
50 |       </indentOptions>
51 |     </codeStyleSettings>
52 |     <codeStyleSettings language="JavaScript">
53 |       <option name="SOFT_MARGINS" value="120" />
54 |     </codeStyleSettings>
55 |     <codeStyleSettings language="TypeScript">
56 |       <option name="SOFT_MARGINS" value="120" />
57 |     </codeStyleSettings>
58 |     <codeStyleSettings language="Vue">
59 |       <option name="SOFT_MARGINS" value="120" />
60 |       <indentOptions>
61 |         <option name="INDENT_SIZE" value="4" />
62 |         <option name="TAB_SIZE" value="4" />
63 |       </indentOptions>
64 |     </codeStyleSettings>
65 |   </code_scheme>
66 | </component>
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  5 | import { z } from 'zod';
  6 | import { registerSfCommands, clearCommandCache, refreshCommandCache, setProjectDirectory, getProjectRoots } from './sfCommands.js';
  7 | import path from 'path';
  8 | import { registerResources } from './resources.js';
  9 | import { extractProjectDirectoryFromMessage } from './utils.js';
 10 | 
 11 | // Create an MCP server
 12 | const server = new McpServer({
 13 |     name: 'Salesforce CLI MCP',
 14 |     version: '1.1.0',
 15 |     description: 'MCP server for Salesforce CLI integration',
 16 | });
 17 | 
 18 | // Only register utility tools that aren't SF CLI commands
 19 | // These are utility functions that extend or manage the MCP server itself
 20 | server.tool('sf_cache_clear', 'Clear the cached SF command metadata to force a refresh', {}, async () => {
 21 |     const result = clearCommandCache();
 22 |     return {
 23 |         content: [
 24 |             {
 25 |                 type: 'text',
 26 |                 text: result
 27 |                     ? 'Command cache cleared successfully.'
 28 |                     : 'Failed to clear command cache or cache did not exist.',
 29 |             },
 30 |         ]
 31 |     };
 32 | });
 33 | 
 34 | server.tool('sf_cache_refresh', 'Refresh the SF command cache by re-scanning all available commands', {}, async () => {
 35 |     const result = refreshCommandCache();
 36 |     return {
 37 |         content: [
 38 |             {
 39 |                 type: 'text',
 40 |                 text: result
 41 |                     ? 'Command cache refreshed successfully. Restart the server to use the new cache.'
 42 |                     : 'Failed to refresh command cache.',
 43 |             },
 44 |         ],
 45 |     };
 46 | });
 47 | 
 48 | // Tools for managing Salesforce project directories (roots)
 49 | 
 50 | // Tool for automatically detecting project directories from messages
 51 | server.tool('sf_detect_project_directory', 'Get instructions for setting up Salesforce project directories for command execution', {}, async () => {
 52 |     // Since we can't access the message in this version of MCP,
 53 |     // we need to rely on the LLM to extract the directory and use sf_set_project_directory
 54 |     
 55 |     return {
 56 |         content: [
 57 |             {
 58 |                 type: 'text',
 59 |                 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".',
 60 |             },
 61 |         ],
 62 |     };
 63 | });
 64 | 
 65 | // Tool for explicitly setting a project directory (root)
 66 | server.tool('sf_set_project_directory', 'Set a Salesforce project directory for command execution context', {
 67 |     directory: z.string().describe('The absolute path to a directory containing an sfdx-project.json file'),
 68 |     name: z.string().optional().describe('Optional name for this project root'),
 69 |     description: z.string().optional().describe('Optional description for this project root'),
 70 |     isDefault: z.boolean().optional().describe('Set this root as the default for command execution')
 71 | }, async (params) => {
 72 |     
 73 |     // Set the project directory with optional metadata
 74 |     const result = setProjectDirectory(params.directory, {
 75 |         name: params.name,
 76 |         description: params.description,
 77 |         isDefault: params.isDefault
 78 |     });
 79 |     
 80 |     return {
 81 |         content: [
 82 |             {
 83 |                 type: 'text',
 84 |                 text: result
 85 |                     ? `Successfully set Salesforce project root: ${params.directory}${params.name ? ` with name "${params.name}"` : ''}${params.isDefault ? ' (default)' : ''}`
 86 |                     : `Failed to set project directory. Make sure the path exists and contains an sfdx-project.json file.`,
 87 |             },
 88 |         ],
 89 |     };
 90 | });
 91 | 
 92 | // Tool for listing configured project roots
 93 | server.tool('sf_list_roots', 'List all configured Salesforce project directories and their metadata', {}, async () => {
 94 |     const roots = getProjectRoots();
 95 |     
 96 |     if (roots.length === 0) {
 97 |         return {
 98 |             content: [
 99 |                 {
100 |                     type: 'text',
101 |                     text: 'No project roots configured. Use sf_set_project_directory to add a project root.'
102 |                 }
103 |             ]
104 |         };
105 |     }
106 |     
107 |     // Format roots list for display
108 |     const rootsList = roots.map(root => (
109 |         `- ${root.name || path.basename(root.path)}${root.isDefault ? ' (default)' : ''}: ${root.path}${root.description ? `\n  Description: ${root.description}` : ''}`
110 |     )).join('\n\n');
111 |     
112 |     return {
113 |         content: [
114 |             {
115 |                 type: 'text',
116 |                 text: `Configured Salesforce project roots:\n\n${rootsList}`
117 |             }
118 |         ]
119 |     };
120 | });
121 | 
122 | // Start the server with stdio transport
123 | // We can't use middleware, so we'll rely on explicit tool use
124 | // The LLM will need to be instructed to look for project directory references
125 | // and call the sf_set_project_directory tool
126 | 
127 | /**
128 |  * Process command line arguments to detect and set project roots
129 |  * All arguments that look like filesystem paths are treated as potential roots
130 |  */
131 | function processRootPaths(): void {
132 |     // Skip the first two arguments (node executable and script path)
133 |     const args = process.argv.slice(2);
134 |     
135 |     if (!args || args.length === 0) {
136 |         console.error('No arguments provided');
137 |         return;
138 |     }
139 | 
140 |     // Filter arguments that appear to be filesystem paths
141 |     // A path typically starts with / or ./ or ../ or ~/ or contains a directory separator
142 |     const rootPaths = args.filter(arg => 
143 |         arg.startsWith('/') || 
144 |         arg.startsWith('./') || 
145 |         arg.startsWith('../') || 
146 |         arg.startsWith('~/') ||
147 |         arg.includes('/') ||
148 |         arg.includes('\\')
149 |     );
150 |     
151 |     if (rootPaths.length === 0) {
152 |         console.error('No project roots identified in CLI arguments');
153 |         return;
154 |     }
155 | 
156 |     console.error(`Configuring ${rootPaths.length} project roots from CLI arguments...`);
157 |     
158 |     // Process each provided path
159 |     for (let i = 0; i < rootPaths.length; i++) {
160 |         const rootPath = rootPaths[i];
161 |         const isDefault = i === 0; // Make the first root the default
162 |         const rootName = `root${i + 1}`;
163 |         
164 |         // Set up this root
165 |         const result = setProjectDirectory(rootPath, {
166 |             name: rootName,
167 |             isDefault,
168 |             description: `CLI-configured root #${i + 1}`
169 |         });
170 |         
171 |         if (result) {
172 |             console.error(`Configured project root #${i + 1}: ${rootPath}`);
173 |         } else {
174 |             console.error(`Failed to configure project root #${i + 1}: ${rootPath}`);
175 |         }
176 |     }
177 | }
178 | 
179 | async function main() {
180 |     try {
181 |         // Process any command line arguments for project roots
182 |         processRootPaths();
183 |         
184 |         // Register documentation resources
185 |         registerResources(server);
186 | 
187 |         // Register all SF CLI commands as tools (dynamic discovery)
188 |         const dynamicToolCount = await registerSfCommands(server);
189 | 
190 |         // Add the utility tools we registered manually
191 |         const totalTools = dynamicToolCount + 5; // sf_cache_clear, sf_cache_refresh, sf_set_project_directory, sf_detect_project_directory, sf_list_roots
192 |         console.error(`Total registered tools: ${totalTools} (${dynamicToolCount} SF CLI tools + 5 utility tools)`);
193 | 
194 |         console.error('Starting Salesforce CLI MCP Server...');
195 |         const transport = new StdioServerTransport();
196 |         await server.connect(transport);
197 |     } catch (err) {
198 |         console.error('Error starting server:', err);
199 |         process.exit(1);
200 |     }
201 | }
202 | 
203 | main();
204 | 
```

--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------

```
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <project version="4">
  3 |   <component name="AutoImportSettings">
  4 |     <option name="autoReloadType" value="SELECTIVE" />
  5 |   </component>
  6 |   <component name="ChangeListManager">
  7 |     <list default="true" id="f109ddcd-c921-4a27-aea6-657a0d3b75de" name="Changes" comment="feat(context directories): All commands require a directory&#10;&#10;to execute from, so that the child_process can CWD there, and the sf cli understand it's project context">
  8 |       <change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
  9 |       <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
 10 |     </list>
 11 |     <option name="SHOW_DIALOG" value="false" />
 12 |     <option name="HIGHLIGHT_CONFLICTS" value="true" />
 13 |     <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
 14 |     <option name="LAST_RESOLUTION" value="IGNORE" />
 15 |   </component>
 16 |   <component name="Git.Settings">
 17 |     <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
 18 |   </component>
 19 |   <component name="GitHubPullRequestSearchHistory">{
 20 |   &quot;lastFilter&quot;: {
 21 |     &quot;state&quot;: &quot;OPEN&quot;,
 22 |     &quot;assignee&quot;: &quot;codefriar&quot;
 23 |   }
 24 | }</component>
 25 |   <component name="GithubPullRequestsUISettings">{
 26 |   &quot;selectedUrlAndAccountId&quot;: {
 27 |     &quot;url&quot;: &quot;[email protected]:codefriar/sf-mcp.git&quot;,
 28 |     &quot;accountId&quot;: &quot;cd1051c7-86d1-42aa-9984-58b0635d53d5&quot;
 29 |   },
 30 |   &quot;recentNewPullRequestHead&quot;: {
 31 |     &quot;server&quot;: {
 32 |       &quot;useHttp&quot;: false,
 33 |       &quot;host&quot;: &quot;github.com&quot;,
 34 |       &quot;port&quot;: null,
 35 |       &quot;suffix&quot;: null
 36 |     },
 37 |     &quot;owner&quot;: &quot;codefriar&quot;,
 38 |     &quot;repository&quot;: &quot;sf-mcp&quot;
 39 |   }
 40 | }</component>
 41 |   <component name="ProjectColorInfo">{
 42 |   &quot;associatedIndex&quot;: 4
 43 | }</component>
 44 |   <component name="ProjectId" id="2urn5n86K5zpk5YZjhjYXG5kRcZ" />
 45 |   <component name="ProjectViewState">
 46 |     <option name="hideEmptyMiddlePackages" value="true" />
 47 |     <option name="showLibraryContents" value="true" />
 48 |   </component>
 49 |   <component name="PropertiesComponent">{
 50 |   &quot;keyToString&quot;: {
 51 |     &quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
 52 |     &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
 53 |     &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
 54 |     &quot;git-widget-placeholder&quot;: &quot;#2 on feat/contextualExecution&quot;,
 55 |     &quot;js.linters.configure.manually.selectedeslint&quot;: &quot;true&quot;,
 56 |     &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
 57 |     &quot;node.js.detected.package.standard&quot;: &quot;true&quot;,
 58 |     &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
 59 |     &quot;node.js.selected.package.eslint&quot;: &quot;/Users/kpoorman/src/sfMcp/node_modules/@eslint/eslintrc&quot;,
 60 |     &quot;node.js.selected.package.standard&quot;: &quot;&quot;,
 61 |     &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
 62 |     &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
 63 |     &quot;settings.editor.selected.configurable&quot;: &quot;settings.javascript.linters.eslint&quot;,
 64 |     &quot;ts.external.directory.path&quot;: &quot;/Applications/IntelliJ IDEA.app/Contents/plugins/javascript-plugin/jsLanguageServicesImpl/external&quot;,
 65 |     &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
 66 |   }
 67 | }</component>
 68 |   <component name="RubyModuleManagerSettings">
 69 |     <option name="blackListedRootsPaths">
 70 |       <list>
 71 |         <option value="$PROJECT_DIR$" />
 72 |       </list>
 73 |     </option>
 74 |   </component>
 75 |   <component name="SharedIndexes">
 76 |     <attachedChunks>
 77 |       <set>
 78 |         <option value="bundled-jdk-9823dce3aa75-a94e463ab2e7-intellij.indexing.shared.core-IU-243.26053.27" />
 79 |         <option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-IU-243.26053.27" />
 80 |       </set>
 81 |     </attachedChunks>
 82 |   </component>
 83 |   <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
 84 |   <component name="TaskManager">
 85 |     <task active="true" id="Default" summary="Default task">
 86 |       <changelist id="f109ddcd-c921-4a27-aea6-657a0d3b75de" name="Changes" comment="" />
 87 |       <created>1743015380248</created>
 88 |       <option name="number" value="Default" />
 89 |       <option name="presentableId" value="Default" />
 90 |       <updated>1743015380248</updated>
 91 |       <workItem from="1743015381427" duration="2834000" />
 92 |       <workItem from="1743030208472" duration="5343000" />
 93 |       <workItem from="1743138461831" duration="925000" />
 94 |       <workItem from="1743139755619" duration="9045000" />
 95 |       <workItem from="1743222211272" duration="953000" />
 96 |       <workItem from="1743606226833" duration="2474000" />
 97 |       <workItem from="1743709358220" duration="3768000" />
 98 |     </task>
 99 |     <task id="LOCAL-00001" summary="feat(tools): autodiscovery of tools&#10;&#10;based on `sf commands --json`">
100 |       <option name="closed" value="true" />
101 |       <created>1743142354973</created>
102 |       <option name="number" value="00001" />
103 |       <option name="presentableId" value="LOCAL-00001" />
104 |       <option name="project" value="LOCAL" />
105 |       <updated>1743142354973</updated>
106 |     </task>
107 |     <task id="LOCAL-00002" summary="chore(lint): prettier and eslint&#10;&#10;rules applied">
108 |       <option name="closed" value="true" />
109 |       <created>1743143924176</created>
110 |       <option name="number" value="00002" />
111 |       <option name="presentableId" value="LOCAL-00002" />
112 |       <option name="project" value="LOCAL" />
113 |       <updated>1743143924176</updated>
114 |     </task>
115 |     <task id="LOCAL-00003" summary="chore(packaging): added standard-release">
116 |       <option name="closed" value="true" />
117 |       <created>1743144159287</created>
118 |       <option name="number" value="00003" />
119 |       <option name="presentableId" value="LOCAL-00003" />
120 |       <option name="project" value="LOCAL" />
121 |       <updated>1743144159287</updated>
122 |     </task>
123 |     <task id="LOCAL-00004" summary="chore(build): build version">
124 |       <option name="closed" value="true" />
125 |       <created>1743144468869</created>
126 |       <option name="number" value="00004" />
127 |       <option name="presentableId" value="LOCAL-00004" />
128 |       <option name="project" value="LOCAL" />
129 |       <updated>1743144468869</updated>
130 |     </task>
131 |     <task id="LOCAL-00005" summary="chore(cleanup): cleanup of code in prep&#10;&#10;for release, and the use of npx">
132 |       <option name="closed" value="true" />
133 |       <created>1743613695726</created>
134 |       <option name="number" value="00005" />
135 |       <option name="presentableId" value="LOCAL-00005" />
136 |       <option name="project" value="LOCAL" />
137 |       <updated>1743613695726</updated>
138 |     </task>
139 |     <task id="LOCAL-00006" summary="feat(context directories): All commands require a directory&#10;&#10;to execute from, so that the child_process can CWD there, and the sf cli understand it's project context">
140 |       <option name="closed" value="true" />
141 |       <created>1743826235821</created>
142 |       <option name="number" value="00006" />
143 |       <option name="presentableId" value="LOCAL-00006" />
144 |       <option name="project" value="LOCAL" />
145 |       <updated>1743826235821</updated>
146 |     </task>
147 |     <option name="localTasksCounter" value="7" />
148 |     <servers />
149 |   </component>
150 |   <component name="TypeScriptGeneratedFilesManager">
151 |     <option name="version" value="3" />
152 |   </component>
153 |   <component name="VcsManagerConfiguration">
154 |     <MESSAGE value="feat(tools): autodiscovery of tools&#10;&#10;based on `sf commands --json`" />
155 |     <MESSAGE value="chore(lint): prettier and eslint&#10;&#10;rules applied" />
156 |     <MESSAGE value="chore(packaging): added standard-release" />
157 |     <MESSAGE value="chore(build): build version" />
158 |     <MESSAGE value="chore(cleanup): cleanup of code in prep&#10;&#10;for release, and the use of npx" />
159 |     <MESSAGE value="feat(context directories): All commands require a directory&#10;&#10;to execute from, so that the child_process can CWD there, and the sf cli understand it's project context" />
160 |     <option name="LAST_COMMIT_MESSAGE" value="feat(context directories): All commands require a directory&#10;&#10;to execute from, so that the child_process can CWD there, and the sf cli understand it's project context" />
161 |   </component>
162 |   <component name="XSLT-Support.FileAssociations.UIState">
163 |     <expand />
164 |     <select />
165 |   </component>
166 | </project>
```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utility functions for working with the Salesforce CLI
  3 |  */
  4 | 
  5 | /**
  6 |  * Parse a user message to look for project directory specification
  7 |  * @param message A message from the user that might contain project directory specification
  8 |  * @returns The extracted directory path, or null if none found
  9 |  */
 10 | export function extractProjectDirectoryFromMessage(message: string): string | null {
 11 |     if (!message) return null;
 12 |     
 13 |     // Common patterns for specifying project directories
 14 |     const patterns = [
 15 |         // "Execute in /path/to/project"
 16 |         /[Ee]xecute\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/,
 17 |         // "Run in /path/to/project"
 18 |         /[Rr]un\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/,
 19 |         // "Use project in /path/to/project"
 20 |         /[Uu]se\s+project\s+(?:in|from|at)\s+(['"]?)([\/~][^\n'"]+)\1/,
 21 |         // "Set project directory to /path/to/project"
 22 |         /[Ss]et\s+project\s+directory\s+(?:to|as)\s+(['"]?)([\/~][^\n'"]+)\1/,
 23 |         // "Project is at /path/to/project"
 24 |         /[Pp]roject\s+(?:is|located)\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/,
 25 |         // "My project is in /path/to/project"
 26 |         /[Mm]y\s+project\s+is\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/,
 27 |         // "/path/to/project is my project"
 28 |         /(['"]?)([\/~][^\n'"]+)\1\s+is\s+my\s+(?:project|directory)/,
 29 |     ];
 30 |     
 31 |     for (const pattern of patterns) {
 32 |         const match = message.match(pattern);
 33 |         if (match) {
 34 |             return match[2];
 35 |         }
 36 |     }
 37 |     
 38 |     return null;
 39 | }
 40 | 
 41 | /**
 42 |  * Formats an object as a string representation of CLI flags
 43 |  * @param flags Key-value pairs of flag names and values
 44 |  * @returns Formatted flags string suitable for command line
 45 |  */
 46 | export function formatFlags(flags: Record<string, any>): string {
 47 |     if (!flags) return '';
 48 | 
 49 |     return Object.entries(flags)
 50 |         .map(([key, value]) => {
 51 |             // Skip undefined/null values
 52 |             if (value === undefined || value === null) return '';
 53 | 
 54 |             // Handle boolean flags
 55 |             if (typeof value === 'boolean') {
 56 |                 return value ? `--${key}` : '';
 57 |             }
 58 | 
 59 |             // Handle arrays (space-separated multi-values)
 60 |             if (Array.isArray(value)) {
 61 |                 return value.map((v) => `--${key}=${escapeValue(v)}`).join(' ');
 62 |             }
 63 | 
 64 |             // Handle objects (JSON stringify)
 65 |             if (typeof value === 'object') {
 66 |                 return `--${key}=${escapeValue(JSON.stringify(value))}`;
 67 |             }
 68 | 
 69 |             // Regular values
 70 |             return `--${key}=${escapeValue(value)}`;
 71 |         })
 72 |         .filter(Boolean)
 73 |         .join(' ');
 74 | }
 75 | 
 76 | /**
 77 |  * Escapes values for command line usage
 78 |  */
 79 | function escapeValue(value: any): string {
 80 |     const stringValue = String(value);
 81 | 
 82 |     // If value contains spaces, wrap in quotes
 83 |     if (stringValue.includes(' ')) {
 84 |         // Escape any existing quotes
 85 |         return `"${stringValue.replace(/"/g, '\\"')}"`;
 86 |     }
 87 | 
 88 |     return stringValue;
 89 | }
 90 | 
 91 | /**
 92 |  * Parses help text to extract structured information about commands or flags
 93 |  * @param helpText Help text from Salesforce CLI
 94 |  * @returns Structured information extracted from help text
 95 |  */
 96 | export function parseHelpText(helpText: string): {
 97 |     description: string;
 98 |     examples: string[];
 99 |     flags: Record<
100 |         string,
101 |         {
102 |             name: string;
103 |             description: string;
104 |             required: boolean;
105 |             type: string;
106 |             char?: string;
107 |         }
108 |     >;
109 | } {
110 |     const description: string[] = [];
111 |     const examples: string[] = [];
112 |     const flags: Record<string, any> = {};
113 | 
114 |     // Split by sections
115 |     const sections = helpText.split(/\n\s*\n/);
116 | 
117 |     // Extract description (usually the first section, skipping DESCRIPTION header if present)
118 |     if (sections.length > 0) {
119 |         let firstSection = sections[0].trim();
120 |         if (firstSection.toUpperCase().startsWith('DESCRIPTION')) {
121 |             firstSection = firstSection.substring(firstSection.indexOf('\n') + 1).trim();
122 |         }
123 |         description.push(firstSection);
124 |     }
125 | 
126 |     // Look for a description section if the first section wasn't clear
127 |     if (description[0]?.length < 10 || description[0]?.toUpperCase().includes('USAGE')) {
128 |         const descSection = sections.find(
129 |             (section) =>
130 |                 section.toUpperCase().startsWith('DESCRIPTION') || section.toUpperCase().includes('\nDESCRIPTION\n')
131 |         );
132 | 
133 |         if (descSection) {
134 |             const descContent = descSection.replace(/DESCRIPTION/i, '').trim();
135 |             if (descContent) {
136 |                 description.push(descContent);
137 |             }
138 |         }
139 |     }
140 | 
141 |     // Look for examples section with improved pattern matching
142 |     const examplePatterns = [/EXAMPLES?/i, /USAGE/i];
143 | 
144 |     for (const pattern of examplePatterns) {
145 |         const exampleSection = sections.find((section) => pattern.test(section));
146 |         if (exampleSection) {
147 |             // Extract examples - look for command lines that start with $ or sf
148 |             const exampleLines = exampleSection
149 |                 .split('\n')
150 |                 .filter((line) => {
151 |                     const trimmed = line.trim();
152 |                     return trimmed.startsWith('$') || trimmed.startsWith('sf ') || /^\s*\d+\.\s+sf\s+/.test(line); // Numbered examples: "1. sf ..."
153 |                 })
154 |                 .map((line) => line.trim().replace(/^\d+\.\s+/, '')); // Remove numbering if present
155 | 
156 |             examples.push(...exampleLines);
157 |         }
158 |     }
159 | 
160 |     // Look for flags section with improved pattern matching
161 |     const flagPatterns = [/FLAGS/i, /OPTIONS/i, /PARAMETERS/i, /ARGUMENTS/i];
162 | 
163 |     for (const pattern of flagPatterns) {
164 |         const flagSections = sections.filter((section) => pattern.test(section));
165 | 
166 |         for (const flagSection of flagSections) {
167 |             // Skip the section header line
168 |             const sectionLines = flagSection.split('\n').slice(1);
169 | 
170 |             // Different patterns for flag lines
171 |             const flagPatterns = [
172 |                 // Pattern 1: Classic -c, --char=<value> Description
173 |                 /^\s*(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:=<?([a-zA-Z0-9_\-\[\]|]+)>?)?\s+(.+)$/,
174 | 
175 |                 // Pattern 2: Indented flag with details (common in newer SF CLI)
176 |                 /^\s+(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)(?:<([a-zA-Z0-9_\-\[\]|]+)>)?\s*\n\s+(.+)/,
177 | 
178 |                 // Pattern 3: Simple flag with no/minimal formatting
179 |                 /^\s*(?:-([a-zA-Z]),\s*)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)?(?:\s*<([a-zA-Z0-9_\-\[\]|]+)>)?\s+(.+)$/,
180 |             ];
181 | 
182 |             // Process the flag section
183 |             let i = 0;
184 |             while (i < sectionLines.length) {
185 |                 const line = sectionLines[i];
186 |                 const nextLine = i < sectionLines.length - 1 ? sectionLines[i + 1] : '';
187 |                 const combinedLines = line + '\n' + nextLine;
188 | 
189 |                 let matched = false;
190 | 
191 |                 // Try all patterns
192 |                 for (const pattern of flagPatterns) {
193 |                     const match = line.match(pattern) || combinedLines.match(pattern);
194 | 
195 |                     if (match) {
196 |                         matched = true;
197 |                         const char = match[1];
198 |                         const name = match[2];
199 |                         const type = match[3] || 'boolean';
200 |                         const description = match[4].trim();
201 | 
202 |                         // Check if this flag is required
203 |                         const required =
204 |                             description.toLowerCase().includes('(required)') ||
205 |                             description.toLowerCase().includes('[required]') ||
206 |                             description.toLowerCase().includes('required:') ||
207 |                             description.toLowerCase().includes('required -');
208 | 
209 |                         // Normalize the type
210 |                         let normalizedType = type.toLowerCase();
211 |                         if (normalizedType.includes('number') || normalizedType.includes('int')) {
212 |                             normalizedType = 'number';
213 |                         } else if (normalizedType.includes('boolean') || normalizedType === 'flag') {
214 |                             normalizedType = 'boolean';
215 |                         } else if (normalizedType.includes('array') || normalizedType.includes('[]')) {
216 |                             normalizedType = 'array';
217 |                         } else if (normalizedType.includes('json') || normalizedType.includes('object')) {
218 |                             normalizedType = 'json';
219 |                         } else {
220 |                             normalizedType = 'string';
221 |                         }
222 | 
223 |                         flags[name] = {
224 |                             name,
225 |                             char,
226 |                             description: description
227 |                                 .replace(/\([Rr]equired\)|\[[Rr]equired\]|[Rr]equired:?/g, '')
228 |                                 .trim(),
229 |                             required,
230 |                             type: normalizedType,
231 |                         };
232 | 
233 |                         // Skip the next line if we matched against a two-line pattern
234 |                         if (combinedLines.match(pattern) && !line.match(pattern)) {
235 |                             i++;
236 |                         }
237 | 
238 |                         break;
239 |                     }
240 |                 }
241 | 
242 |                 // If no pattern matched and this line looks like it might be a flag
243 |                 if (!matched && (line.includes('--') || line.trim().startsWith('-'))) {
244 |                     console.error(`No pattern matched for potential flag line: "${line.trim()}"`);
245 |                 }
246 | 
247 |                 i++;
248 |             }
249 |         }
250 |     }
251 | 
252 |     return {
253 |         description: description.join('\n\n'),
254 |         examples,
255 |         flags,
256 |     };
257 | }
258 | 
```

--------------------------------------------------------------------------------
/.idea/AugmentWebviewStateStore.xml:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <project version="4">
 3 |   <component name="AugmentWebviewStateStore">
 4 |     <option name="stateMap">
 5 |       <map>
 6 |         <entry key="CHAT_STATE" value="eyJjdXJyZW50Q29udmVyc2F0aW9uSWQiOiJiOTcyZWQwZi0yNmUzLTQ4NjktOTY4YS01YzEyZTIyOTZhNzEiLCJjb252ZXJzYXRpb25zIjp7ImI5NzJlZDBmLTI2ZTMtNDg2OS05NjhhLTVjMTJlMjI5NmE3MSI6eyJpZCI6ImI5NzJlZDBmLTI2ZTMtNDg2OS05NjhhLTVjMTJlMjI5NmE3MSIsImNyZWF0ZWRBdElzbyI6IjIwMjUtMDQtMDhUMDU6NDI6MDIuOTIzWiIsImxhc3RJbnRlcmFjdGVkQXRJc28iOiIyMDI1LTA0LTA4VDA1OjQzOjQxLjUxMFoiLCJjaGF0SGlzdG9yeSI6W3sic3RhdHVzIjoic3VjY2VzcyIsInJlcXVlc3RfaWQiOiJjODczNTEyMS1lMzkwLTRkNWItYmE0YS0zODJmMTg0NGY5MGUiLCJyZXF1ZXN0X21lc3NhZ2UiOiJQbGVhc2Ugc2NhbiB0aGUgcHJvamVjdCwgYW5kIHVwZGF0ZSB0aGUgUkVBRE1FIGFwcHJvcHJpYXRlbHkuIE1ha2Ugc3VyZSB0byB1cGRhdGUgdGhlIGluc3RhbGxhdGlvbiBpbnN0cnVjdGlvbnMgYW5kIGNvZGUgdG8gc3VwcG9ydCBydW5uaW5nIHZpYSBucHggYW5kIGluc3RhbGxpbmcgdmlhIG5wbSAiLCJyaWNoX3RleHRfanNvbl9yZXByIjp7InR5cGUiOiJkb2MiLCJjb250ZW50IjpbeyJ0eXBlIjoicGFyYWdyYXBoIiwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0IjoiUGxlYXNlIHNjYW4gdGhlIHByb2plY3QsIGFuZCB1cGRhdGUgdGhlIFJFQURNRSBhcHByb3ByaWF0ZWx5LiBNYWtlIHN1cmUgdG8gdXBkYXRlIHRoZSBpbnN0YWxsYXRpb24gaW5zdHJ1Y3Rpb25zIGFuZCBjb2RlIHRvIHN1cHBvcnQgcnVubmluZyB2aWEgbnB4IGFuZCBpbnN0YWxsaW5nIHZpYSBucG0gIn1dfV19LCJtZW50aW9uZWRfaXRlbXMiOltdLCJzdHJ1Y3R1cmVkX291dHB1dF9ub2RlcyI6W3siaWQiOjAsInR5cGUiOjAsImNvbnRlbnQiOiJCYXNlZCBvbiB0aGUgY29kZWJhc2UsIEknbGwgdXBkYXRlIHRoZSBSRUFETUUgdG8gaW5jbHVkZSBwcm9wZXIgaW5zdGFsbGF0aW9uIGluc3RydWN0aW9ucyBmb3IgcnVubmluZyB2aWEgbnB4IGFuZCBpbnN0YWxsaW5nIHZpYSBucG0uIFRoZSBjdXJyZW50IFJFQURNRSBhbHJlYWR5IGhhcyBzb21lIG9mIHRoaXMgaW5mb3JtYXRpb24sIGJ1dCBJJ2xsIGVuaGFuY2UgaXQgdG8gYmUgbW9yZSBjb21wbGV0ZSBhbmQgYWNjdXJhdGUuXG5cbjxhdWdtZW50X2NvZGVfc25pcHBldCBwYXRoPVwiUkVBRE1FLm1kXCIgbW9kZT1cIkVESVRcIj5cbmBgYG1hcmtkb3duXG4jIFNhbGVzZm9yY2UgQ0xJIE1DUCBTZXJ2ZXJcblxuTW9kZWwgQ29udGV4dCBQcm90b2NvbCAoTUNQKSBzZXJ2ZXIgZm9yIHByb3ZpZGluZyBTYWxlc2ZvcmNlIENMSSBmdW5jdGlvbmFsaXR5IHRvIExMTSB0b29scyBsaWtlIENsYXVkZSBEZXNrdG9wLlxuXG4jIyBPdmVydmlld1xuXG5UaGlzIE1DUCBzZXJ2ZXIgd3JhcHMgdGhlIFNhbGVzZm9yY2UgQ0xJIChgc2ZgKSBjb21tYW5kLWxpbmUgdG9vbCBhbmQgZXhwb3NlcyBpdHMgY29tbWFuZHMgYXMgTUNQIHRvb2xzIGFuZCByZXNvdXJjZXMsIGFsbG93aW5nIExMTS1wb3dlcmVkIGFnZW50cyB0bzpcblxuLSBWaWV3IGhlbHAgaW5mb3JtYXRpb24gYWJvdXQgU2FsZXNmb3JjZSBDTEkgdG9waWNzIGFuZCBjb21tYW5kc1xuLSBFeGVjdXRlIFNhbGVzZm9yY2UgQ0xJIGNvbW1hbmRzIHdpdGggYXBwcm9wcmlhdGUgcGFyYW1ldGVyc1xuLSBMZXZlcmFnZSBTYWxlc2ZvcmNlIENMSSBjYXBhYmlsaXRpZXMgaW4gQUkgd29ya2Zsb3dzXG5cbiMjIFJlcXVpcmVtZW50c1xuXG4tIE5vZGUuanMgMTgrIGFuZCBucG1cbi0gU2FsZXNmb3JjZSBDTEkgKGBzZmApIGluc3RhbGxlZCBhbmQgY29uZmlndXJlZFxuLSBZb3VyIFNhbGVzZm9yY2Ugb3JnIGNyZWRlbnRpYWxzIGNvbmZpZ3VyZWQgaW4gdGhlIENMSVxuXG4jIyBJbnN0YWxsYXRpb25cblxuIyMjIE9wdGlvbiAxOiBJbnN0YWxsIGZyb20gbnBtIChyZWNvbW1lbmRlZClcblxuYGBgYmFzaFxuIyBJbnN0YWxsIGdsb2JhbGx5XG5ucG0gaW5zdGFsbCAtZyBzZi1tY3BcblxuIyBPciBydW4gZGlyZWN0bHkgd2l0aCBucHhcbm5weCBzZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5gYGBcblxuIyMjIE9wdGlvbiAyOiBDbG9uZSBhbmQgYnVpbGRcblxuYGBgYmFzaFxuIyBDbG9uZSB0aGUgcmVwb3NpdG9yeVxuZ2l0IGNsb25lIDxyZXBvc2l0b3J5LXVybD5cbmNkIHNmLW1jcFxuXG4jIEluc3RhbGwgZGVwZW5kZW5jaWVzXG5ucG0gaW5zdGFsbFxuXG4jIEJ1aWxkIHRoZSBwcm9qZWN0XG5ucG0gcnVuIGJ1aWxkXG5gYGBcblxuIyMgVXNhZ2VcblxuIyMjIFN0YXJ0aW5nIHRoZSBzZXJ2ZXJcblxuYGBgYmFzaFxuIyBJZiBpbnN0YWxsZWQgZ2xvYmFsbHlcbnNmLW1jcCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBJZiB1c2luZyBucHhcbm5weCBzZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgSWYgY2xvbmVkIGxvY2FsbHlcbm5wbSBzdGFydCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBXaXRoIHNwZWNpZmljIHByb2plY3Qgcm9vdHNcbnNmLW1jcCAvcGF0aC90by9wcm9qZWN0MSAvcGF0aC90by9wcm9qZWN0MlxuIyBvciB1c2luZyB0aGUgY29udmVuaWVuY2Ugc2NyaXB0IGlmIGNsb25lZCBsb2NhbGx5XG5ucG0gcnVuIHdpdGgtcm9vdHMgL3BhdGgvdG8vcHJvamVjdDEgL3BhdGgvdG8vcHJvamVjdDJcbmBgYFxuXG5UaGUgTUNQIHNlcnZlciB1c2VzIHN0ZGlvIHRyYW5zcG9ydCwgd2hpY2ggY2FuIGJlIHVzZWQgd2l0aCBNQ1AgY2xpZW50cyBsaWtlIHRoZSBbTUNQIEluc3BlY3Rvcl0oaHR0cHM6Ly9naXRodWIuY29tL21vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3Rvcikgb3IgQ2xhdWRlIERlc2t0b3AuXG5cbiMjIyBUZXN0aW5nIHdpdGggTUNQIEluc3BlY3RvclxuXG5Zb3UgY2FuIHRlc3QgdGhlIHNlcnZlciB1c2luZyB0aGUgTUNQIEluc3BlY3RvcjpcblxuYGBgYmFzaFxuIyBUZXN0IHdpdGggSW5zcGVjdG9yXG5ucHggQG1vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3RvciBucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuXG4jIE9yIGlmIGNsb25lZCBsb2NhbGx5XG5ucHggQG1vZGVsY29udGV4dHByb3RvY29sL2luc3BlY3RvciBub2RlIGJ1aWxkL2luZGV4LmpzIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuYGBgXG5cbiMjIyBDb25maWd1cmluZyBpbiBDbGF1ZGUgRGVza3RvcFxuXG5UbyBjb25maWd1cmUgdGhpcyBNQ1AgaW4gQ2xhdWRlIERlc2t0b3AncyBgLmNsYXVkZS5qc29uYCBjb25maWd1cmF0aW9uOlxuXG5gYGBqc29uXG57XG4gIFwidG9vbHNcIjoge1xuICAgIFwic2FsZXNmb3JjZVwiOiB7XG4gICAgICBcImNvbW1hbmRcIjogXCJucHhcIixcbiAgICAgIFwiYXJnc1wiOiBbXG4gICAgICAgIFwiLXlcIixcbiAgICAgICAgXCJzZi1tY3BcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MVwiLFxuICAgICAgICBcIi9wYXRoL3RvL3Byb2plY3QyXCJcbiAgICAgIF1cbiAgICB9XG4gIH1cbn1cbmBgYFxuXG5Vc2luZyBhIGxvY2FsbHkgY2xvbmVkIHZlcnNpb246XG5cbmBgYGpzb25cbntcbiAgXCJ0b29sc1wiOiB7XG4gICAgXCJzYWxlc2ZvcmNlXCI6IHtcbiAgICAgIFwiY29tbWFuZFwiOiBcIi9wYXRoL3RvL25vZGVcIixcbiAgICAgIFwiYXJnc1wiOiBbXG4gICAgICAgIFwiL3BhdGgvdG8vc2YtbWNwL2J1aWxkL2luZGV4LmpzXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDFcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MlwiXG4gICAgICBdXG4gICAgfVxuICB9XG59XG5gYGBcblxuIyMjIERldmVsb3BtZW50XG5cbmBgYGJhc2hcbiMgV2F0Y2ggbW9kZSAocmVjb21waWxlcyBvbiBmaWxlIGNoYW5nZXMpXG5ucG0gcnVuIGRldlxuXG4jIEluIGFub3RoZXIgdGVybWluYWxcbm5wbSBzdGFydCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cbmBgYFxuYGBgXG48L2F1Z21lbnRfY29kZV9zbmlwcGV0PlxuXG5JJ3ZlIHVwZGF0ZWQgdGhlIFJFQURNRSB3aXRoIGNvbXByZWhlbnNpdmUgaW5zdGFsbGF0aW9uIGFuZCB1c2FnZSBpbnN0cnVjdGlvbnMgdGhhdCBzdXBwb3J0OlxuXG4xLiBJbnN0YWxsaW5nIGdsb2JhbGx5IHZpYSBucG0gKGBucG0gaW5zdGFsbCAtZyBzZi1tY3BgKVxuMi4gUnVubmluZyBkaXJlY3RseSB3aXRoIG5weCAoYG5weCBzZi1tY3BgKVxuMy4gQ2xvbmluZyBhbmQgYnVpbGRpbmcgbG9jYWxseVxuNC4gVGVzdGluZyB3aXRoIHRoZSBNQ1AgSW5zcGVjdG9yXG41LiBDb25maWd1cmluZyBpbiBDbGF1ZGUgRGVza3RvcCB3aXRoIGJvdGggbnB4IGFuZCBsb2NhbCBvcHRpb25zXG5cblRoZSBpbnN0cnVjdGlvbnMgbWFpbnRhaW4gYWxsIHRoZSBleGlzdGluZyBmdW5jdGlvbmFsaXR5IHdoaWxlIG1ha2luZyBpdCBjbGVhciBob3cgdG8gdXNlIHRoZSBwYWNrYWdlIHZpYSBucG0gYW5kIG5weC4gSSd2ZSBhbHNvIG9yZ2FuaXplZCB0aGUgaW5zdGFsbGF0aW9uIG9wdGlvbnMgdG8gaGlnaGxpZ2h0IHRoZSBucG0gaW5zdGFsbGF0aW9uIGFzIHRoZSByZWNvbW1lbmRlZCBhcHByb2FjaCBmb3IgbW9zdCB1c2Vycy5cbiJ9LHsiaWQiOjIsInR5cGUiOjIsImNvbnRlbnQiOiIifSx7ImlkIjozLCJ0eXBlIjozLCJjb250ZW50IjoiUkVBRE1FLm1kOjAtNzYwXG5SRUFETUUubWQ6NzYwLTEzODhcblJFQURNRS5tZDoxMzg4LTIwNDFcblJFQURNRS5tZDoyMDQxLTI1NDFcblJFQURNRS5tZDoyNTQxLTMxNDhcblJFQURNRS5tZDozMTQ4LTM2MDVcblJFQURNRS5tZDozNjA1LTQyOTVcblJFQURNRS5tZDo0Mjk1LTQ4NjJcblJFQURNRS5tZDo0ODYyLTU1ODdcblJFQURNRS5tZDo1NTg3LTYyMThcblJFQURNRS5tZDo2MjE4LTY5NjZcblJFQURNRS5tZDo2OTY2LTc2MTRcblJFQURNRS5tZDo3NjE0LTc3OTlcbmxsbXMtZnVsbC50eHQ6MjQ5MTAtMjU1NzJcbmxsbXMtZnVsbC50eHQ6OTIxNDktOTI4NjJcbmxsbXMtZnVsbC50eHQ6OTI4NjItOTM1NjRcbmxsbXMtZnVsbC50eHQ6OTM1NjQtOTQyODJcbmxsbXMtZnVsbC50eHQ6MTAxMzA4LTEwMjAwOFxubGxtcy1mdWxsLnR4dDoxMDI0NzYtMTAzMjIzXG5sbG1zLWZ1bGwudHh0OjEwOTEyNy0xMDk5ODRcbmxsbXMtZnVsbC50eHQ6MTE3Nzk1LTExODYwNlxubGxtcy1mdWxsLnR4dDoxMjE3MDktMTIyNTY2XG5sbG1zLWZ1bGwudHh0OjEyODk5OS0xMjk3NjVcbmxsbXMtZnVsbC50eHQ6MTMzMTcxLTEzNDA5MlxubGxtcy1mdWxsLnR4dDoxMzYxMjItMTM2ODA4XG5sbG1zLWZ1bGwudHh0OjE0NzI1NC0xNDc5MTRcbmxsbXMtZnVsbC50eHQ6MTU2MTU5LTE1Njk3MFxubGxtcy1mdWxsLnR4dDoxNjA0MjctMTYwNjc5XG5sbG1zLWZ1bGwudHh0OjE2MDY3OS0xNjE1MzBcbmxsbXMtZnVsbC50eHQ6MTY5NDQyLTE3MDI1M1xubGxtcy1mdWxsLnR4dDoxODU1NDYtMTg2MTA5XG5sbG1zLWZ1bGwudHh0OjIwNDQ2OS0yMDUyNzFcbmxsbXMtZnVsbC50eHQ6MjA1MjcxLTIwNjAyOFxubGxtcy1mdWxsLnR4dDoyMjA4MDktMjIxNDQxXG5sbG1zLWZ1bGwudHh0OjIyNjA2NC0yMjY1NDJcbkNMQVVERS5tZDowLTM4NlxucnVuLnNoOjAtNDgwXG5zcmMvaW5kZXgudHM6MC02MDVcbnNyYy9pbmRleC50czozMjAwLTQwNDMifV0sInNlZW5fc3RhdGUiOiJzZWVuIiwicmVzcG9uc2VfdGV4dCI6IkJhc2VkIG9uIHRoZSBjb2RlYmFzZSwgSSdsbCB1cGRhdGUgdGhlIFJFQURNRSB0byBpbmNsdWRlIHByb3BlciBpbnN0YWxsYXRpb24gaW5zdHJ1Y3Rpb25zIGZvciBydW5uaW5nIHZpYSBucHggYW5kIGluc3RhbGxpbmcgdmlhIG5wbS4gVGhlIGN1cnJlbnQgUkVBRE1FIGFscmVhZHkgaGFzIHNvbWUgb2YgdGhpcyBpbmZvcm1hdGlvbiwgYnV0IEknbGwgZW5oYW5jZSBpdCB0byBiZSBtb3JlIGNvbXBsZXRlIGFuZCBhY2N1cmF0ZS5cblxuYGBgYG1hcmtkb3duIHBhdGg9UkVBRE1FLm1kIG1vZGU9RURJVFxuIyBTYWxlc2ZvcmNlIENMSSBNQ1AgU2VydmVyXG5cbk1vZGVsIENvbnRleHQgUHJvdG9jb2wgKE1DUCkgc2VydmVyIGZvciBwcm92aWRpbmcgU2FsZXNmb3JjZSBDTEkgZnVuY3Rpb25hbGl0eSB0byBMTE0gdG9vbHMgbGlrZSBDbGF1ZGUgRGVza3RvcC5cblxuIyMgT3ZlcnZpZXdcblxuVGhpcyBNQ1Agc2VydmVyIHdyYXBzIHRoZSBTYWxlc2ZvcmNlIENMSSAoYHNmYCkgY29tbWFuZC1saW5lIHRvb2wgYW5kIGV4cG9zZXMgaXRzIGNvbW1hbmRzIGFzIE1DUCB0b29scyBhbmQgcmVzb3VyY2VzLCBhbGxvd2luZyBMTE0tcG93ZXJlZCBhZ2VudHMgdG86XG5cbi0gVmlldyBoZWxwIGluZm9ybWF0aW9uIGFib3V0IFNhbGVzZm9yY2UgQ0xJIHRvcGljcyBhbmQgY29tbWFuZHNcbi0gRXhlY3V0ZSBTYWxlc2ZvcmNlIENMSSBjb21tYW5kcyB3aXRoIGFwcHJvcHJpYXRlIHBhcmFtZXRlcnNcbi0gTGV2ZXJhZ2UgU2FsZXNmb3JjZSBDTEkgY2FwYWJpbGl0aWVzIGluIEFJIHdvcmtmbG93c1xuXG4jIyBSZXF1aXJlbWVudHNcblxuLSBOb2RlLmpzIDE4KyBhbmQgbnBtXG4tIFNhbGVzZm9yY2UgQ0xJIChgc2ZgKSBpbnN0YWxsZWQgYW5kIGNvbmZpZ3VyZWRcbi0gWW91ciBTYWxlc2ZvcmNlIG9yZyBjcmVkZW50aWFscyBjb25maWd1cmVkIGluIHRoZSBDTElcblxuIyMgSW5zdGFsbGF0aW9uXG5cbiMjIyBPcHRpb24gMTogSW5zdGFsbCBmcm9tIG5wbSAocmVjb21tZW5kZWQpXG5cbmBgYGJhc2hcbiMgSW5zdGFsbCBnbG9iYWxseVxubnBtIGluc3RhbGwgLWcgc2YtbWNwXG5cbiMgT3IgcnVuIGRpcmVjdGx5IHdpdGggbnB4XG5ucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuYGBgXG5cbiMjIyBPcHRpb24gMjogQ2xvbmUgYW5kIGJ1aWxkXG5cbmBgYGJhc2hcbiMgQ2xvbmUgdGhlIHJlcG9zaXRvcnlcbmdpdCBjbG9uZSA8cmVwb3NpdG9yeS11cmw+XG5jZCBzZi1tY3BcblxuIyBJbnN0YWxsIGRlcGVuZGVuY2llc1xubnBtIGluc3RhbGxcblxuIyBCdWlsZCB0aGUgcHJvamVjdFxubnBtIHJ1biBidWlsZFxuYGBgXG5cbiMjIFVzYWdlXG5cbiMjIyBTdGFydGluZyB0aGUgc2VydmVyXG5cbmBgYGJhc2hcbiMgSWYgaW5zdGFsbGVkIGdsb2JhbGx5XG5zZi1tY3AgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgSWYgdXNpbmcgbnB4XG5ucHggc2YtbWNwIFtvcHRpb25hbCBwcm9qZWN0IHJvb3RzLi4uXVxuXG4jIElmIGNsb25lZCBsb2NhbGx5XG5ucG0gc3RhcnQgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5cbiMgV2l0aCBzcGVjaWZpYyBwcm9qZWN0IHJvb3RzXG5zZi1tY3AgL3BhdGgvdG8vcHJvamVjdDEgL3BhdGgvdG8vcHJvamVjdDJcbiMgb3IgdXNpbmcgdGhlIGNvbnZlbmllbmNlIHNjcmlwdCBpZiBjbG9uZWQgbG9jYWxseVxubnBtIHJ1biB3aXRoLXJvb3RzIC9wYXRoL3RvL3Byb2plY3QxIC9wYXRoL3RvL3Byb2plY3QyXG5gYGBcblxuVGhlIE1DUCBzZXJ2ZXIgdXNlcyBzdGRpbyB0cmFuc3BvcnQsIHdoaWNoIGNhbiBiZSB1c2VkIHdpdGggTUNQIGNsaWVudHMgbGlrZSB0aGUgW01DUCBJbnNwZWN0b3JdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3IpIG9yIENsYXVkZSBEZXNrdG9wLlxuXG4jIyMgVGVzdGluZyB3aXRoIE1DUCBJbnNwZWN0b3JcblxuWW91IGNhbiB0ZXN0IHRoZSBzZXJ2ZXIgdXNpbmcgdGhlIE1DUCBJbnNwZWN0b3I6XG5cbmBgYGJhc2hcbiMgVGVzdCB3aXRoIEluc3BlY3RvclxubnB4IEBtb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3IgbnB4IHNmLW1jcCBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cblxuIyBPciBpZiBjbG9uZWQgbG9jYWxseVxubnB4IEBtb2RlbGNvbnRleHRwcm90b2NvbC9pbnNwZWN0b3Igbm9kZSBidWlsZC9pbmRleC5qcyBbb3B0aW9uYWwgcHJvamVjdCByb290cy4uLl1cbmBgYFxuXG4jIyMgQ29uZmlndXJpbmcgaW4gQ2xhdWRlIERlc2t0b3BcblxuVG8gY29uZmlndXJlIHRoaXMgTUNQIGluIENsYXVkZSBEZXNrdG9wJ3MgYC5jbGF1ZGUuanNvbmAgY29uZmlndXJhdGlvbjpcblxuYGBganNvblxue1xuICBcInRvb2xzXCI6IHtcbiAgICBcInNhbGVzZm9yY2VcIjoge1xuICAgICAgXCJjb21tYW5kXCI6IFwibnB4XCIsXG4gICAgICBcImFyZ3NcIjogW1xuICAgICAgICBcIi15XCIsXG4gICAgICAgIFwic2YtbWNwXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDFcIixcbiAgICAgICAgXCIvcGF0aC90by9wcm9qZWN0MlwiXG4gICAgICBdXG4gICAgfVxuICB9XG59XG5gYGBcblxuVXNpbmcgYSBsb2NhbGx5IGNsb25lZCB2ZXJzaW9uOlxuXG5gYGBqc29uXG57XG4gIFwidG9vbHNcIjoge1xuICAgIFwic2FsZXNmb3JjZVwiOiB7XG4gICAgICBcImNvbW1hbmRcIjogXCIvcGF0aC90by9ub2RlXCIsXG4gICAgICBcImFyZ3NcIjogW1xuICAgICAgICBcIi9wYXRoL3RvL3NmLW1jcC9idWlsZC9pbmRleC5qc1wiLFxuICAgICAgICBcIi9wYXRoL3RvL3Byb2plY3QxXCIsXG4gICAgICAgIFwiL3BhdGgvdG8vcHJvamVjdDJcIlxuICAgICAgXVxuICAgIH1cbiAgfVxufVxuYGBgXG5cbiMjIyBEZXZlbG9wbWVudFxuXG5gYGBiYXNoXG4jIFdhdGNoIG1vZGUgKHJlY29tcGlsZXMgb24gZmlsZSBjaGFuZ2VzKVxubnBtIHJ1biBkZXZcblxuIyBJbiBhbm90aGVyIHRlcm1pbmFsXG5ucG0gc3RhcnQgW29wdGlvbmFsIHByb2plY3Qgcm9vdHMuLi5dXG5gYGBcbmBgYGBcblxuSSd2ZSB1cGRhdGVkIHRoZSBSRUFETUUgd2l0aCBjb21wcmVoZW5zaXZlIGluc3RhbGxhdGlvbiBhbmQgdXNhZ2UgaW5zdHJ1Y3Rpb25zIHRoYXQgc3VwcG9ydDpcblxuMS4gSW5zdGFsbGluZyBnbG9iYWxseSB2aWEgbnBtIChgbnBtIGluc3RhbGwgLWcgc2YtbWNwYClcbjIuIFJ1bm5pbmcgZGlyZWN0bHkgd2l0aCBucHggKGBucHggc2YtbWNwYClcbjMuIENsb25pbmcgYW5kIGJ1aWxkaW5nIGxvY2FsbHlcbjQuIFRlc3Rpbmcgd2l0aCB0aGUgTUNQIEluc3BlY3RvclxuNS4gQ29uZmlndXJpbmcgaW4gQ2xhdWRlIERlc2t0b3Agd2l0aCBib3RoIG5weCBhbmQgbG9jYWwgb3B0aW9uc1xuXG5UaGUgaW5zdHJ1Y3Rpb25zIG1haW50YWluIGFsbCB0aGUgZXhpc3RpbmcgZnVuY3Rpb25hbGl0eSB3aGlsZSBtYWtpbmcgaXQgY2xlYXIgaG93IHRvIHVzZSB0aGUgcGFja2FnZSB2aWEgbnBtIGFuZCBucHguIEkndmUgYWxzbyBvcmdhbml6ZWQgdGhlIGluc3RhbGxhdGlvbiBvcHRpb25zIHRvIGhpZ2hsaWdodCB0aGUgbnBtIGluc3RhbGxhdGlvbiBhcyB0aGUgcmVjb21tZW5kZWQgYXBwcm9hY2ggZm9yIG1vc3QgdXNlcnMuXG4iLCJ3b3Jrc3BhY2VfZmlsZV9jaHVua3MiOlt7ImNoYXJfc3RhcnQiOiIwIiwiY2hhcl9lbmQiOiI3NjAiLCJibG9iX25hbWUiOiIwZDY2OWYwNjRjM2I4OTcyNWFiZTYyYmI5YmU2N2JmM2UwZDQzMGVjOWIzYjlmMWExNzBlNTdjMjdjOTAxYzE0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoiUkVBRE1FLm1kIn19LHsiY2hhcl9zdGFydCI6Ijc2MCIsImNoYXJfZW5kIjoiMTM4OCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMTM4OCIsImNoYXJfZW5kIjoiMjA0MSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjA0MSIsImNoYXJfZW5kIjoiMjU0MSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjU0MSIsImNoYXJfZW5kIjoiMzE0OCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMzE0OCIsImNoYXJfZW5kIjoiMzYwNSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMzYwNSIsImNoYXJfZW5kIjoiNDI5NSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNDI5NSIsImNoYXJfZW5kIjoiNDg2MiIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNDg2MiIsImNoYXJfZW5kIjoiNTU4NyIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNTU4NyIsImNoYXJfZW5kIjoiNjIxOCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNjIxOCIsImNoYXJfZW5kIjoiNjk2NiIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNjk2NiIsImNoYXJfZW5kIjoiNzYxNCIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiNzYxNCIsImNoYXJfZW5kIjoiNzc5OSIsImJsb2JfbmFtZSI6IjBkNjY5ZjA2NGMzYjg5NzI1YWJlNjJiYjliZTY3YmYzZTBkNDMwZWM5YjNiOWYxYTE3MGU1N2MyN2M5MDFjMTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJSRUFETUUubWQifX0seyJjaGFyX3N0YXJ0IjoiMjQ5MTAiLCJjaGFyX2VuZCI6IjI1NTcyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTIxNDkiLCJjaGFyX2VuZCI6IjkyODYyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTI4NjIiLCJjaGFyX2VuZCI6IjkzNTY0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiOTM1NjQiLCJjaGFyX2VuZCI6Ijk0MjgyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTAxMzA4IiwiY2hhcl9lbmQiOiIxMDIwMDgiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMDI0NzYiLCJjaGFyX2VuZCI6IjEwMzIyMyIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjEwOTEyNyIsImNoYXJfZW5kIjoiMTA5OTg0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTE3Nzk1IiwiY2hhcl9lbmQiOiIxMTg2MDYiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMjE3MDkiLCJjaGFyX2VuZCI6IjEyMjU2NiIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjEyODk5OSIsImNoYXJfZW5kIjoiMTI5NzY1IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTMzMTcxIiwiY2hhcl9lbmQiOiIxMzQwOTIiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxMzYxMjIiLCJjaGFyX2VuZCI6IjEzNjgwOCIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjE0NzI1NCIsImNoYXJfZW5kIjoiMTQ3OTE0IiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTU2MTU5IiwiY2hhcl9lbmQiOiIxNTY5NzAiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxNjA0MjciLCJjaGFyX2VuZCI6IjE2MDY3OSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjE2MDY3OSIsImNoYXJfZW5kIjoiMTYxNTMwIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMTY5NDQyIiwiY2hhcl9lbmQiOiIxNzAyNTMiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIxODU1NDYiLCJjaGFyX2VuZCI6IjE4NjEwOSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjIwNDQ2OSIsImNoYXJfZW5kIjoiMjA1MjcxIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMjA1MjcxIiwiY2hhcl9lbmQiOiIyMDYwMjgiLCJibG9iX25hbWUiOiJhYTQ1YTM2ZjIyMTgzMDIxNzA5MzY5MjVjNWM3N2E2MGRlZTg2ZGYyNmE1NGI5Njc1ZTBhNDEyOTdlOTk3YzU0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoibGxtcy1mdWxsLnR4dCJ9fSx7ImNoYXJfc3RhcnQiOiIyMjA4MDkiLCJjaGFyX2VuZCI6IjIyMTQ0MSIsImJsb2JfbmFtZSI6ImFhNDVhMzZmMjIxODMwMjE3MDkzNjkyNWM1Yzc3YTYwZGVlODZkZjI2YTU0Yjk2NzVlMGE0MTI5N2U5OTdjNTQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJsbG1zLWZ1bGwudHh0In19LHsiY2hhcl9zdGFydCI6IjIyNjA2NCIsImNoYXJfZW5kIjoiMjI2NTQyIiwiYmxvYl9uYW1lIjoiYWE0NWEzNmYyMjE4MzAyMTcwOTM2OTI1YzVjNzdhNjBkZWU4NmRmMjZhNTRiOTY3NWUwYTQxMjk3ZTk5N2M1NCIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6ImxsbXMtZnVsbC50eHQifX0seyJjaGFyX3N0YXJ0IjoiMCIsImNoYXJfZW5kIjoiMzg2IiwiYmxvYl9uYW1lIjoiMTFjMjk0MDU4YmM0NTA3MWExZTM3NGRjMzFmNTBlMWM4ZTRjMTBiZDE0ZmEwYWZiNDRkZWE1MmRkZmNkYWYxOSIsImZpbGUiOnsicmVwb1Jvb3QiOiIiLCJwYXRoTmFtZSI6IkNMQVVERS5tZCJ9fSx7ImNoYXJfc3RhcnQiOiIwIiwiY2hhcl9lbmQiOiI0ODAiLCJibG9iX25hbWUiOiJhMDMwODQ3YzgwNmMwZDYxYjkzYzIwZjQyZjI1ZDMwMjZiMzBjOWY0NGY5ZWU0ZGE4OTlkYTRjMjY5MTYyYmY0IiwiZmlsZSI6eyJyZXBvUm9vdCI6IiIsInBhdGhOYW1lIjoicnVuLnNoIn19LHsiY2hhcl9zdGFydCI6IjAiLCJjaGFyX2VuZCI6IjYwNSIsImJsb2JfbmFtZSI6IjliYTkwYzZmZGRkYjM0NDU2YWY0Y2M4YWQ2MzVmMTVjNjhjMDQ5ZDhhZGI4MzZhZDYwM2E0YWM3MTFmODYxMmQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJzcmMvaW5kZXgudHMifX0seyJjaGFyX3N0YXJ0IjoiMzIwMCIsImNoYXJfZW5kIjoiNDA0MyIsImJsb2JfbmFtZSI6IjliYTkwYzZmZGRkYjM0NDU2YWY0Y2M4YWQ2MzVmMTVjNjhjMDQ5ZDhhZGI4MzZhZDYwM2E0YWM3MTFmODYxMmQiLCJmaWxlIjp7InJlcG9Sb290IjoiIiwicGF0aE5hbWUiOiJzcmMvaW5kZXgudHMifX1dfV0sImZlZWRiYWNrU3RhdGVzIjp7InRlbXAtZmUtNWRmNTU4ZGUtMjk0MC00MzFiLTgyOGEtYTA5NzFjMTk1Y2FmIjp7InNlbGVjdGVkUmF0aW5nIjowLCJmZWVkYmFja05vdGUiOiIifX0sInRvb2xVc2VTdGF0ZXMiOnt9LCJkcmFmdEV4Y2hhbmdlIjp7InJlcXVlc3RfbWVzc2FnZSI6IiIsInJpY2hfdGV4dF9qc29uX3JlcHIiOnsidHlwZSI6ImRvYyIsImNvbnRlbnQiOlt7InR5cGUiOiJwYXJhZ3JhcGgifV19LCJzdGF0dXMiOiJkcmFmdCJ9LCJyZXF1ZXN0SWRzIjpbXSwiaXNQaW5uZWQiOmZhbHNlLCJpc1NoYXJlYWJsZSI6dHJ1ZSwiZXh0cmFEYXRhIjp7Imhhc0RpcnR5RWRpdHMiOmZhbHNlfX19LCJpc0NvbnRleHRQYW5lT3BlbiI6ZmFsc2UsImlzQWN0aW9uc1BhbmVPcGVuIjpmYWxzZSwiYWdlbnRFeGVjdXRpb25Nb2RlIjoibWFudWFsIiwiaXNBZ2VudEVkaXRzQ29sbGFwc2VkIjp0cnVlfQ==" />
 7 |       </map>
 8 |     </option>
 9 |   </component>
10 | </project>
```

--------------------------------------------------------------------------------
/src/sfCommands.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execSync } from 'child_process';
  2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  3 | import { z } from 'zod';
  4 | import { formatFlags } from './utils.js';
  5 | import fs from 'fs';
  6 | import path from 'path';
  7 | import os from 'os';
  8 | 
  9 | /**
 10 |  * Represents a Salesforce CLI command
 11 |  */
 12 | interface SfCommand {
 13 |     id: string;
 14 |     name: string;
 15 |     description: string;
 16 |     fullCommand: string;
 17 |     flags: SfFlag[];
 18 |     topic?: string;
 19 | }
 20 | 
 21 | /**
 22 |  * Represents a flag for an SF command
 23 |  */
 24 | interface SfFlag {
 25 |     name: string;
 26 |     char?: string;
 27 |     description: string;
 28 |     required: boolean;
 29 |     type: string;
 30 |     options?: string[];
 31 |     default?: string | boolean | number;
 32 | }
 33 | 
 34 | /**
 35 |  * Interface for the JSON format returned by 'sf commands --json'
 36 |  */
 37 | interface SfCommandJsonEntry {
 38 |     id: string;
 39 |     summary: string;
 40 |     description: string;
 41 |     aliases?: string[];
 42 |     flags: Record<
 43 |         string,
 44 |         {
 45 |             name: string;
 46 |             description: string;
 47 |             type: string;
 48 |             required?: boolean;
 49 |             helpGroup?: string;
 50 |             options?: string[];
 51 |             default?: string | boolean | number;
 52 |             char?: string;
 53 |         }
 54 |     >;
 55 |     [key: string]: any;
 56 | }
 57 | 
 58 | /**
 59 |  * Cache structure for storing discovered SF commands
 60 |  */
 61 | interface SfCommandCache {
 62 |     version: string;
 63 |     timestamp: number;
 64 |     commands: SfCommand[];
 65 | }
 66 | 
 67 | /**
 68 |  * List of topics to ignore during command discovery
 69 |  */
 70 | const IGNORED_TOPICS = ['help', 'which', 'whatsnew', 'alias'];
 71 | 
 72 | /**
 73 |  * Path to the cache file
 74 |  */
 75 | const CACHE_DIR = path.join(os.homedir(), '.sf-mcp');
 76 | const CACHE_FILE = path.join(CACHE_DIR, 'command-cache.json');
 77 | const CACHE_MAX_AGE = 86400 * 7 * 1000; // 1 week in milliseconds
 78 | 
 79 | /**
 80 |  * Clear the command cache
 81 |  */
 82 | export function clearCommandCache(): boolean {
 83 |     try {
 84 |         if (fs.existsSync(CACHE_FILE)) {
 85 |             fs.unlinkSync(CACHE_FILE);
 86 |             console.error(`Removed cache file: ${CACHE_FILE}`);
 87 |             return true;
 88 |         } else {
 89 |             console.error(`Cache file does not exist: ${CACHE_FILE}`);
 90 |             return false;
 91 |         }
 92 |     } catch (error) {
 93 |         console.error('Error clearing command cache:', error);
 94 |         return false;
 95 |     }
 96 | }
 97 | 
 98 | /**
 99 |  * Manually force the cache to refresh
100 |  */
101 | export function refreshCommandCache(): boolean {
102 |     try {
103 |         // Clear existing cache
104 |         if (fs.existsSync(CACHE_FILE)) {
105 |             fs.unlinkSync(CACHE_FILE);
106 |         }
107 | 
108 |         // Create a fresh cache
109 |         console.error('Refreshing SF command cache...');
110 | 
111 |         // Get all commands directly from sf commands --json
112 |         const commands = getAllSfCommands();
113 |         console.error(`Found ${commands.length} total commands for cache refresh`);
114 | 
115 |         // Save the cache
116 |         saveCommandCache(commands);
117 |         console.error('Cache refresh complete!');
118 | 
119 |         return true;
120 |     } catch (error) {
121 |         console.error('Error refreshing command cache:', error);
122 |         return false;
123 |     }
124 | }
125 | 
126 | // Get the full path to the sf command
127 | const SF_BINARY_PATH = (() => {
128 |     try {
129 |         // Try to find the sf binary in common locations
130 |         const possiblePaths = [
131 |             '/Users/kpoorman/.volta/bin/sf', // The path we found earlier
132 |             '/usr/local/bin/sf',
133 |             '/usr/bin/sf',
134 |             '/opt/homebrew/bin/sf',
135 |             process.env.HOME + '/.npm/bin/sf',
136 |             process.env.HOME + '/bin/sf',
137 |             process.env.HOME + '/.nvm/versions/node/*/bin/sf',
138 |         ];
139 | 
140 |         for (const path of possiblePaths) {
141 |             try {
142 |                 if (
143 |                     execSync(`[ -x "${path}" ] && echo "exists"`, {
144 |                         encoding: 'utf8',
145 |                     }).trim() === 'exists'
146 |                 ) {
147 |                     return path;
148 |                 }
149 |             } catch (e) {
150 |                 // Path doesn't exist or isn't executable, try the next one
151 |             }
152 |         }
153 | 
154 |         // If we didn't find it in a known location, try to get it from the PATH
155 |         return 'sf';
156 |     } catch (e) {
157 |         console.error("Unable to locate sf binary, falling back to 'sf'");
158 |         return 'sf';
159 |     }
160 | })();
161 | 
162 | /**
163 |  * Execute an sf command and return the results
164 |  * @param command The sf command to run
165 |  * @returns The stdout output from the command
166 |  */
167 | // Store the user-provided project directories (roots)
168 | interface ProjectRoot {
169 |     path: string;
170 |     name?: string;
171 |     description?: string;
172 |     isDefault?: boolean;
173 | }
174 | 
175 | const projectRoots: ProjectRoot[] = [];
176 | let defaultRootPath: string | null = null;
177 | 
178 | /**
179 |  * Validate a directory is a valid Salesforce project
180 |  * @param directory The directory to validate
181 |  * @returns boolean indicating if valid
182 |  */
183 | function isValidSalesforceProject(directory: string): boolean {
184 |     const projectFilePath = path.join(directory, 'sfdx-project.json');
185 |     return fs.existsSync(directory) && fs.existsSync(projectFilePath);
186 | }
187 | 
188 | /**
189 |  * Get all configured project roots
190 |  * @returns Array of project roots
191 |  */
192 | export function getProjectRoots(): ProjectRoot[] {
193 |     return [...projectRoots];
194 | }
195 | 
196 | /**
197 |  * Get the default project directory (for backward compatibility)
198 |  * @returns The default project directory or null if none set
199 |  */
200 | export function getDefaultProjectDirectory(): string | null {
201 |     return defaultRootPath;
202 | }
203 | 
204 | /**
205 |  * Set the Salesforce project directory to use for commands
206 |  * @param directory The directory containing sfdx-project.json
207 |  * @param options Optional parameters (name, description, isDefault)
208 |  * @returns boolean indicating success
209 |  */
210 | export function setProjectDirectory(
211 |     directory: string, 
212 |     options: { name?: string; description?: string; isDefault?: boolean } = {}
213 | ): boolean {
214 |     try {
215 |         // Validate that the directory exists and contains an sfdx-project.json file
216 |         if (!isValidSalesforceProject(directory)) {
217 |             console.error(`Invalid Salesforce project: ${directory}`);
218 |             return false;
219 |         }
220 |         
221 |         // Check if this root already exists
222 |         const existingIndex = projectRoots.findIndex(root => root.path === directory);
223 |         
224 |         if (existingIndex >= 0) {
225 |             // Update existing root with new options
226 |             projectRoots[existingIndex] = {
227 |                 ...projectRoots[existingIndex],
228 |                 ...options,
229 |                 path: directory
230 |             };
231 |             
232 |             // If this is now the default root, update defaultRootPath
233 |             if (options.isDefault) {
234 |                 // Remove default flag from other roots
235 |                 projectRoots.forEach((root, idx) => {
236 |                     if (idx !== existingIndex) {
237 |                         root.isDefault = false;
238 |                     }
239 |                 });
240 |                 defaultRootPath = directory;
241 |             }
242 |             
243 |             console.error(`Updated Salesforce project root: ${directory}`);
244 |         } else {
245 |             // Add as new root
246 |             const isDefault = options.isDefault ?? (projectRoots.length === 0);
247 |             
248 |             projectRoots.push({
249 |                 path: directory,
250 |                 name: options.name || path.basename(directory),
251 |                 description: options.description,
252 |                 isDefault
253 |             });
254 |             
255 |             // If this is now the default root, update defaultRootPath
256 |             if (isDefault) {
257 |                 // Remove default flag from other roots
258 |                 projectRoots.forEach((root, idx) => {
259 |                     if (idx !== projectRoots.length - 1) {
260 |                         root.isDefault = false;
261 |                     }
262 |                 });
263 |                 defaultRootPath = directory;
264 |             }
265 |             
266 |             console.error(`Added Salesforce project root: ${directory}`);
267 |         }
268 |         
269 |         // Always ensure we have exactly one default root if any roots exist
270 |         if (projectRoots.length > 0 && !projectRoots.some(root => root.isDefault)) {
271 |             projectRoots[0].isDefault = true;
272 |             defaultRootPath = projectRoots[0].path;
273 |         }
274 |         
275 |         return true;
276 |     } catch (error) {
277 |         console.error('Error setting project directory:', error);
278 |         return false;
279 |     }
280 | }
281 | 
282 | /**
283 |  * Checks if a command requires a Salesforce project context
284 |  * @param command The SF command to check
285 |  * @returns True if the command requires a Salesforce project context
286 |  */
287 | function requiresSalesforceProjectContext(command: string): boolean {
288 |     // List of commands or command prefixes that require a Salesforce project context
289 |     const projectContextCommands = [
290 |         'project deploy',
291 |         'project retrieve',
292 |         'project delete',
293 |         'project convert',
294 |         'package version create',
295 |         'package1 version create',
296 |         'source',
297 |         'mdapi',
298 |         'apex',
299 |         'lightning',
300 |         'schema generate'
301 |     ];
302 |     
303 |     // Check if the command matches any of the project context commands
304 |     return projectContextCommands.some(contextCmd => command.startsWith(contextCmd));
305 | }
306 | 
307 | /**
308 |  * Execute an sf command and return the results
309 |  * @param command The sf command to run
310 |  * @param rootName Optional specific root name to use for execution
311 |  * @returns The stdout output from the command
312 |  */
313 | export function executeSfCommand(command: string, rootName?: string): string {
314 |     try {
315 |         console.error(`Executing: ${SF_BINARY_PATH} ${command}`);
316 |         
317 |         // Check if target-org parameter is 'default' and replace with the default org
318 |         if (command.includes('--target-org default') || command.includes('--target-org=default')) {
319 |             // Get the default org from sf org list
320 |             const orgListOutput = execSync(`"${SF_BINARY_PATH}" org list --json`, {
321 |                 encoding: 'utf8',
322 |                 maxBuffer: 10 * 1024 * 1024,
323 |             });
324 |             
325 |             const orgList = JSON.parse(orgListOutput);
326 |             let defaultUsername = '';
327 |             
328 |             // Look for the default org across different org types
329 |             for (const orgType of ['nonScratchOrgs', 'scratchOrgs', 'sandboxes']) {
330 |                 if (orgList.result[orgType]) {
331 |                     const defaultOrg = orgList.result[orgType].find((org: any) => org.isDefaultUsername);
332 |                     if (defaultOrg) {
333 |                         defaultUsername = defaultOrg.username;
334 |                         break;
335 |                     }
336 |                 }
337 |             }
338 |             
339 |             if (defaultUsername) {
340 |                 // Replace 'default' with the actual default org username
341 |                 command = command.replace(/--target-org[= ]default/, `--target-org ${defaultUsername}`);
342 |                 console.error(`Using default org: ${defaultUsername}`);
343 |             }
344 |         }
345 |         
346 |         // Determine which project directory to use
347 |         let projectDir: string | null = null;
348 |         
349 |         // If rootName specified, find that specific root
350 |         if (rootName) {
351 |             const root = projectRoots.find(r => r.name === rootName);
352 |             if (root) {
353 |                 projectDir = root.path;
354 |                 console.error(`Using specified root "${rootName}" at ${projectDir}`);
355 |             } else {
356 |                 console.error(`Root "${rootName}" not found, falling back to default root`);
357 |                 // Fall back to default
358 |                 projectDir = defaultRootPath;
359 |             }
360 |         } else {
361 |             // Use default root
362 |             projectDir = defaultRootPath;
363 |         }
364 |         
365 |         // Check if this command requires a Salesforce project context and we don't have a project directory
366 |         if (requiresSalesforceProjectContext(command) && !projectDir) {
367 |             return `This command requires a Salesforce project context (sfdx-project.json).
368 | Please specify a project directory using the format:
369 | "Execute in <directory_path>" or "Use project in <directory_path>"`;
370 |         }
371 |         
372 |         try {
373 |             // Always execute in project directory if available
374 |             if (projectDir) {
375 |                 console.error(`Executing command in Salesforce project directory: ${projectDir}`);
376 |                 
377 |                 // Execute the command within the specified project directory
378 |                 const result = execSync(`"${SF_BINARY_PATH}" ${command}`, {
379 |                     encoding: 'utf8',
380 |                     maxBuffer: 10 * 1024 * 1024,
381 |                     env: {
382 |                         ...process.env,
383 |                         PATH: process.env.PATH,
384 |                     },
385 |                     cwd: projectDir,
386 |                     stdio: ['pipe', 'pipe', 'pipe'] // Capture stderr too
387 |                 });
388 |                 
389 |                 console.error('Command execution successful');
390 |                 return result;
391 |             } else {
392 |                 // Standard execution for when no project directory is set
393 |                 return execSync(`"${SF_BINARY_PATH}" ${command}`, {
394 |                     encoding: 'utf8',
395 |                     maxBuffer: 10 * 1024 * 1024,
396 |                     env: {
397 |                         ...process.env,
398 |                         PATH: process.env.PATH,
399 |                     },
400 |                 });
401 |             }
402 |         } catch (execError: any) {
403 |             console.error(`Error executing command: ${execError.message}`);
404 |             
405 |             // Capture both stdout and stderr for better error diagnostics
406 |             let errorOutput = '';
407 |             if (execError.stdout) {
408 |                 errorOutput += execError.stdout;
409 |             }
410 |             if (execError.stderr) {
411 |                 errorOutput += `\n\nError details: ${execError.stderr}`;
412 |             }
413 |             
414 |             if (errorOutput) {
415 |                 console.error(`Command output: ${errorOutput}`);
416 |                 return errorOutput;
417 |             }
418 |             
419 |             return `Error executing command: ${execError.message}`;
420 |         }
421 |     } catch (error: any) {
422 |         console.error(`Top-level error executing command: ${error.message}`);
423 |         
424 |         // Capture both stdout and stderr 
425 |         let errorOutput = '';
426 |         if (error.stdout) {
427 |             errorOutput += error.stdout;
428 |         }
429 |         if (error.stderr) {
430 |             errorOutput += `\n\nError details: ${error.stderr}`;
431 |         }
432 |         
433 |         if (errorOutput) {
434 |             console.error(`Command output: ${errorOutput}`);
435 |             return errorOutput;
436 |         }
437 |         
438 |         return `Error executing command: ${error.message}`;
439 |     }
440 | }
441 | 
442 | /**
443 |  * Get all Salesforce CLI commands using 'sf commands --json'
444 |  */
445 | function getAllSfCommands(): SfCommand[] {
446 |     try {
447 |         console.error("Fetching all SF CLI commands via 'sf commands --json'...");
448 | 
449 |         // Execute the command to get all commands in JSON format
450 |         const commandsJson = executeSfCommand('commands --json');
451 |         const allCommands: SfCommandJsonEntry[] = JSON.parse(commandsJson);
452 | 
453 |         console.error(`Found ${allCommands.length} total commands from 'sf commands --json'`);
454 | 
455 |         // Filter out commands from ignored topics
456 |         const filteredCommands = allCommands.filter((cmd) => {
457 |             if (!cmd.id) return false;
458 | 
459 |             // For commands with colons (topic:command format), check if the topic should be ignored
460 |             if (cmd.id.includes(':')) {
461 |                 const topic = cmd.id.split(':')[0].toLowerCase();
462 |                 return !IGNORED_TOPICS.includes(topic);
463 |             }
464 | 
465 |             // For standalone commands, check if the command itself should be ignored
466 |             return !IGNORED_TOPICS.includes(cmd.id.toLowerCase());
467 |         });
468 | 
469 |         console.error(`After filtering ignored topics, ${filteredCommands.length} commands remain`);
470 | 
471 |         // Transform JSON commands to SfCommand format
472 |         const sfCommands: SfCommand[] = filteredCommands.map((jsonCmd) => {
473 |             // Parse the command structure from its ID
474 |             const commandParts = jsonCmd.id.split(':');
475 |             const isTopicCommand = commandParts.length > 1;
476 | 
477 |             // For commands like "apex:run", extract name and topic
478 |             let commandName = isTopicCommand ? commandParts[commandParts.length - 1] : jsonCmd.id;
479 |             let topic = isTopicCommand ? commandParts.slice(0, commandParts.length - 1).join(':') : undefined;
480 | 
481 |             // The full command with spaces instead of colons for execution
482 |             const fullCommand = jsonCmd.id.replace(/:/g, ' ');
483 | 
484 |             // Convert flags from JSON format to SfFlag format
485 |             const flags: SfFlag[] = Object.entries(jsonCmd.flags || {}).map(([flagName, flagDetails]) => {
486 |                 return {
487 |                     name: flagName,
488 |                     char: flagDetails.char,
489 |                     description: flagDetails.description || '',
490 |                     required: !!flagDetails.required,
491 |                     type: flagDetails.type || 'string',
492 |                     options: flagDetails.options,
493 |                     default: flagDetails.default,
494 |                 };
495 |             });
496 | 
497 |             return {
498 |                 id: jsonCmd.id,
499 |                 name: commandName,
500 |                 description: jsonCmd.summary || jsonCmd.description || jsonCmd.id,
501 |                 fullCommand,
502 |                 flags,
503 |                 topic,
504 |             };
505 |         });
506 | 
507 |         console.error(`Successfully processed ${sfCommands.length} commands`);
508 |         return sfCommands;
509 |     } catch (error) {
510 |         console.error('Error getting SF commands:', error);
511 |         return [];
512 |     }
513 | }
514 | 
515 | /**
516 |  * Convert an SF command to a schema object for validation
517 |  */
518 | function commandToZodSchema(command: SfCommand): Record<string, z.ZodTypeAny> {
519 |     const schemaObj: Record<string, z.ZodTypeAny> = {};
520 | 
521 | 
522 |     for (const flag of command.flags) {
523 |         let flagSchema: z.ZodTypeAny;
524 | 
525 |         // Convert flag type to appropriate Zod schema
526 |         switch (flag.type) {
527 |             case 'number':
528 |             case 'integer':
529 |             case 'int':
530 |                 flagSchema = z.number();
531 |                 break;
532 |             case 'boolean':
533 |             case 'flag':
534 |                 flagSchema = z.boolean();
535 |                 break;
536 |             case 'array':
537 |             case 'string[]':
538 |                 flagSchema = z.array(z.string());
539 |                 break;
540 |             case 'json':
541 |             case 'object':
542 |                 flagSchema = z.union([z.string(), z.record(z.any())]);
543 |                 break;
544 |             case 'file':
545 |             case 'directory':
546 |             case 'filepath':
547 |             case 'path':
548 |             case 'email':
549 |             case 'url':
550 |             case 'date':
551 |             case 'datetime':
552 |             case 'id':
553 |             default:
554 |                 // For options-based flags, create an enum schema
555 |                 if (flag.options && flag.options.length > 0) {
556 |                     flagSchema = z.enum(flag.options as [string, ...string[]]);
557 |                 } else {
558 |                     flagSchema = z.string();
559 |                 }
560 |         }
561 | 
562 |         // Add description
563 |         if (flag.description) {
564 |             flagSchema = flagSchema.describe(flag.description);
565 |         }
566 | 
567 |         // Make required or optional based on flag definition
568 |         schemaObj[flag.name] = flag.required ? flagSchema : flagSchema.optional();
569 |     }
570 | 
571 |     return schemaObj;
572 | }
573 | 
574 | /**
575 |  * Get the SF CLI version to use for cache validation
576 |  */
577 | function getSfVersion(): string {
578 |     try {
579 |         const versionOutput = executeSfCommand('--version');
580 |         const versionMatch = versionOutput.match(/sf\/(\d+\.\d+\.\d+)/);
581 |         return versionMatch ? versionMatch[1] : 'unknown';
582 |     } catch (error) {
583 |         console.error('Error getting SF version:', error);
584 |         return 'unknown';
585 |     }
586 | }
587 | 
588 | /**
589 |  * Saves the SF command data to cache
590 |  */
591 | function saveCommandCache(commands: SfCommand[]): void {
592 |     try {
593 |         // Create cache directory if it doesn't exist
594 |         if (!fs.existsSync(CACHE_DIR)) {
595 |             fs.mkdirSync(CACHE_DIR, { recursive: true });
596 |         }
597 | 
598 |         const sfVersion = getSfVersion();
599 |         const cache: SfCommandCache = {
600 |             version: sfVersion,
601 |             timestamp: Date.now(),
602 |             commands,
603 |         };
604 | 
605 |         fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
606 |         console.error(`Command cache saved to ${CACHE_FILE} (SF version: ${sfVersion})`);
607 |     } catch (error) {
608 |         console.error('Error saving command cache:', error);
609 |     }
610 | }
611 | 
612 | /**
613 |  * Loads the SF command data from cache
614 |  * Returns null if cache is missing, invalid, or expired
615 |  */
616 | function loadCommandCache(): SfCommand[] | null {
617 |     try {
618 |         if (!fs.existsSync(CACHE_FILE)) {
619 |             console.error('Command cache file does not exist');
620 |             return null;
621 |         }
622 | 
623 |         const cacheData = fs.readFileSync(CACHE_FILE, 'utf8');
624 |         const cache = JSON.parse(cacheData) as SfCommandCache;
625 | 
626 |         // Validate cache structure
627 |         if (!cache.version || !cache.timestamp || !Array.isArray(cache.commands)) {
628 |             console.error('Invalid cache structure');
629 |             return null;
630 |         }
631 | 
632 |         // Check if cache is expired
633 |         const now = Date.now();
634 |         if (now - cache.timestamp > CACHE_MAX_AGE) {
635 |             console.error('Cache is expired');
636 |             return null;
637 |         }
638 | 
639 |         // Verify that SF version matches
640 |         const currentVersion = getSfVersion();
641 |         if (cache.version !== currentVersion) {
642 |             console.error(`Cache version mismatch. Cache: ${cache.version}, Current: ${currentVersion}`);
643 |             return null;
644 |         }
645 | 
646 |         console.error(
647 |             `Using command cache from ${new Date(cache.timestamp).toLocaleString()} (SF version: ${cache.version})`
648 |         );
649 |         console.error(`Found ${cache.commands.length} commands in cache`);
650 | 
651 |         return cache.commands;
652 |     } catch (error) {
653 |         console.error('Error loading command cache:', error);
654 |         return null;
655 |     }
656 | }
657 | 
658 | /**
659 |  * Register all SF commands as MCP tools
660 |  * @returns The total number of registered tools
661 |  */
662 | export async function registerSfCommands(server: McpServer): Promise<number> {
663 |     try {
664 |         console.error('Starting SF command registration');
665 | 
666 |         // Try to load commands from cache first
667 |         let sfCommands = loadCommandCache();
668 | 
669 |         // If cache doesn't exist or is invalid, fetch commands directly
670 |         if (!sfCommands) {
671 |             console.error('Cache not available or invalid, fetching commands directly');
672 |             sfCommands = getAllSfCommands();
673 | 
674 |             // Save to cache for future use
675 |             saveCommandCache(sfCommands);
676 |         }
677 | 
678 |         // List of manually defined tools to avoid conflicts
679 |         // Only includes the utility cache management tools
680 |         const reservedTools = ['sf_cache_clear', 'sf_cache_refresh'];
681 | 
682 |         // Keep track of registered tools and aliases to avoid duplicates
683 |         const registeredTools = new Set<string>(reservedTools);
684 |         const registeredAliases = new Set<string>();
685 | 
686 |         // Register all commands as tools
687 |         let toolCount = 0;
688 | 
689 |         for (const command of sfCommands) {
690 |             try {
691 |                 // Create appropriate MCP-valid tool name
692 |                 let toolName: string;
693 | 
694 |                 if (command.topic) {
695 |                     // For commands with topics, format as "sf_topic_command"
696 |                     toolName = `sf_${command.topic.replace(/:/g, '_')}_${command.name}`.replace(/[^a-zA-Z0-9_-]/g, '_');
697 |                 } else {
698 |                     // Standalone commands - sf_command
699 |                     toolName = `sf_${command.name}`.replace(/[^a-zA-Z0-9_-]/g, '_');
700 |                 }
701 | 
702 |                 // Ensure tool name meets length requirements (1-64 characters)
703 |                 if (toolName.length > 64) {
704 |                     toolName = toolName.substring(0, 64);
705 |                 }
706 | 
707 |                 // Skip if this tool name conflicts with a manually defined tool or is already registered
708 |                 if (registeredTools.has(toolName)) {
709 |                     console.error(`Skipping ${toolName} because it's already registered`);
710 |                     continue;
711 |                 }
712 | 
713 |                 const zodSchema = commandToZodSchema(command);
714 | 
715 |                 // Register the command as a tool with description
716 |                 server.tool(toolName, command.description, zodSchema, async (flags) => {
717 |                     const flagsStr = formatFlags(flags);
718 |                     const commandStr = `${command.fullCommand} ${flagsStr}`;
719 | 
720 |                     console.error(`Executing: sf ${commandStr}`);
721 |                     try {
722 |                         const output = executeSfCommand(commandStr);
723 |                         // Check if the output indicates an error but was returned as normal output
724 |                         if (output && (output.includes('Error executing command') || output.includes('Error details:'))) {
725 |                             console.error(`Command returned error: ${output}`);
726 |                             return {
727 |                                 content: [
728 |                                     {
729 |                                         type: 'text',
730 |                                         text: output,
731 |                                     },
732 |                                 ],
733 |                                 isError: true,
734 |                             };
735 |                         }
736 |                         
737 |                         return {
738 |                             content: [
739 |                                 {
740 |                                     type: 'text',
741 |                                     text: output,
742 |                                 },
743 |                             ],
744 |                         };
745 |                     } catch (error: any) {
746 |                         console.error(`Error executing ${commandStr}:`, error);
747 |                         const errorMessage = error.stdout || error.stderr || error.message || 'Unknown error';
748 |                         return {
749 |                             content: [
750 |                                 {
751 |                                     type: 'text',
752 |                                     text: `Error: ${errorMessage}`,
753 |                                 },
754 |                             ],
755 |                             isError: true,
756 |                         };
757 |                     }
758 |                 });
759 | 
760 |                 // Add to registered tools set and increment counter
761 |                 registeredTools.add(toolName);
762 |                 toolCount++;
763 | 
764 |                 // For nested commands, create simplified aliases when possible
765 |                 // (e.g., sf_get for sf_apex_log_get)
766 |                 if (command.topic && command.topic.includes(':') && command.name.length > 2) {
767 |                     const simplifiedName = command.name.toLowerCase();
768 |                     const simplifiedToolName = `sf_${simplifiedName}`.replace(/[^a-zA-Z0-9_-]/g, '_');
769 | 
770 |                     // Skip if the simplified name is already registered as a tool or alias
771 |                     if (registeredTools.has(simplifiedToolName) || registeredAliases.has(simplifiedToolName)) {
772 |                         continue;
773 |                     }
774 | 
775 |                     // Register simplified alias with description
776 |                     try {
777 |                         server.tool(simplifiedToolName, `Alias for ${command.description}`, zodSchema, async (flags) => {
778 |                             const flagsStr = formatFlags(flags);
779 |                             const commandStr = `${command.fullCommand} ${flagsStr}`;
780 | 
781 |                             console.error(`Executing (via alias ${simplifiedToolName}): sf ${commandStr}`);
782 |                             try {
783 |                                 const output = executeSfCommand(commandStr);
784 |                                 // Check if the output indicates an error but was returned as normal output
785 |                                 if (output && (output.includes('Error executing command') || output.includes('Error details:'))) {
786 |                                     console.error(`Command returned error: ${output}`);
787 |                                     return {
788 |                                         content: [
789 |                                             {
790 |                                                 type: 'text',
791 |                                                 text: output,
792 |                                             },
793 |                                         ],
794 |                                         isError: true,
795 |                                     };
796 |                                 }
797 |                                 
798 |                                 return {
799 |                                     content: [
800 |                                         {
801 |                                             type: 'text',
802 |                                             text: output,
803 |                                         },
804 |                                     ],
805 |                                 };
806 |                             } catch (error: any) {
807 |                                 console.error(`Error executing ${commandStr}:`, error);
808 |                                 const errorMessage = error.stdout || error.stderr || error.message || 'Unknown error';
809 |                                 return {
810 |                                     content: [
811 |                                         {
812 |                                             type: 'text',
813 |                                             text: `Error: ${errorMessage}`,
814 |                                         },
815 |                                     ],
816 |                                     isError: true,
817 |                                 };
818 |                             }
819 |                         });
820 | 
821 |                         // Add alias to tracking sets and increment counter
822 |                         registeredAliases.add(simplifiedToolName);
823 |                         registeredTools.add(simplifiedToolName);
824 |                         toolCount++;
825 |                         console.error(`Registered alias ${simplifiedToolName} for ${toolName}`);
826 |                     } catch (err) {
827 |                         console.error(`Error registering alias ${simplifiedToolName}:`, err);
828 |                     }
829 |                 }
830 |             } catch (err) {
831 |                 console.error(`Error registering tool for command ${command.id}:`, err);
832 |             }
833 |         }
834 | 
835 |         const totalTools = toolCount + registeredAliases.size;
836 |         console.error(
837 |             `Registration complete. Registered ${totalTools} tools (${toolCount} commands and ${registeredAliases.size} aliases).`
838 |         );
839 | 
840 |         // Return the count for the main server to use
841 |         return totalTools;
842 |     } catch (error) {
843 |         console.error('Error registering SF commands:', error);
844 |         return 0;
845 |     }
846 | }
847 | 
```