#
tokens: 35988/50000 13/13 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── docs
│   ├── analysis.md
│   ├── code-restructuring.md
│   ├── error-handling-and-logging.md
│   ├── feature-improvements.md
│   ├── implementation-roadmap.md
│   ├── README.md
│   ├── security-and-authentication.md
│   └── testing-strategy.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

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

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```

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

```markdown
  1 | # ERPNext MCP Server
  2 | 
  3 | A Model Context Protocol server for ERPNext integration
  4 | 
  5 | This is a TypeScript-based MCP server that provides integration with ERPNext/Frappe API. It enables AI assistants to interact with ERPNext data and functionality through the Model Context Protocol.
  6 | 
  7 | ## Features
  8 | 
  9 | ### Resources
 10 | - Access ERPNext documents via `erpnext://{doctype}/{name}` URIs
 11 | - JSON format for structured data access
 12 | 
 13 | ### Tools
 14 | - `authenticate_erpnext` - Authenticate with ERPNext using username and password
 15 | - `get_documents` - Get a list of documents for a specific doctype
 16 | - `create_document` - Create a new document in ERPNext
 17 | - `update_document` - Update an existing document in ERPNext
 18 | - `run_report` - Run an ERPNext report
 19 | - `get_doctype_fields` - Get fields list for a specific DocType
 20 | - `get_doctypes` - Get a list of all available DocTypes
 21 | 
 22 | ## Configuration
 23 | 
 24 | The server requires the following environment variables:
 25 | - `ERPNEXT_URL` - The base URL of your ERPNext instance
 26 | - `ERPNEXT_API_KEY` (optional) - API key for authentication
 27 | - `ERPNEXT_API_SECRET` (optional) - API secret for authentication
 28 | 
 29 | ## Development
 30 | 
 31 | Install dependencies:
 32 | ```bash
 33 | npm install
 34 | ```
 35 | 
 36 | Build the server:
 37 | ```bash
 38 | npm run build
 39 | ```
 40 | 
 41 | For development with auto-rebuild:
 42 | ```bash
 43 | npm run watch
 44 | ```
 45 | 
 46 | ## Installation
 47 | 
 48 | To use with Claude Desktop, add the server config:
 49 | 
 50 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 51 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
 52 | 
 53 | ```json
 54 | {
 55 |   "mcpServers": {
 56 |     "erpnext": {
 57 |       "command": "node",
 58 |       "args": ["/path/to/erpnext-server/build/index.js"],
 59 |       "env": {
 60 |         "ERPNEXT_URL": "http://your-erpnext-instance.com",
 61 |         "ERPNEXT_API_KEY": "your-api-key",
 62 |         "ERPNEXT_API_SECRET": "your-api-secret"
 63 |       }
 64 |     }
 65 |   }
 66 | }
 67 | ```
 68 | 
 69 | To use with Claude in VSCode, add the server config to:
 70 | 
 71 | On MacOS: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
 72 | On Windows: `%APPDATA%/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
 73 | 
 74 | ### Debugging
 75 | 
 76 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
 77 | 
 78 | ```bash
 79 | npm run inspector
 80 | ```
 81 | 
 82 | The Inspector will provide a URL to access debugging tools in your browser.
 83 | 
 84 | ## Usage Examples
 85 | 
 86 | ### Authentication
 87 | ```
 88 | <use_mcp_tool>
 89 | <server_name>erpnext</server_name>
 90 | <tool_name>authenticate_erpnext</tool_name>
 91 | <arguments>
 92 | {
 93 |   "username": "your-username",
 94 |   "password": "your-password"
 95 | }
 96 | </arguments>
 97 | </use_mcp_tool>
 98 | ```
 99 | 
100 | ### Get Customer List
101 | ```
102 | <use_mcp_tool>
103 | <server_name>erpnext</server_name>
104 | <tool_name>get_documents</tool_name>
105 | <arguments>
106 | {
107 |   "doctype": "Customer"
108 | }
109 | </arguments>
110 | </use_mcp_tool>
111 | ```
112 | 
113 | ### Get Customer Details
114 | ```
115 | <access_mcp_resource>
116 | <server_name>erpnext</server_name>
117 | <uri>erpnext://Customer/CUSTOMER001</uri>
118 | </access_mcp_resource>
119 | ```
120 | 
121 | ### Create New Item
122 | ```
123 | <use_mcp_tool>
124 | <server_name>erpnext</server_name>
125 | <tool_name>create_document</tool_name>
126 | <arguments>
127 | {
128 |   "doctype": "Item",
129 |   "data": {
130 |     "item_code": "ITEM001",
131 |     "item_name": "Test Item",
132 |     "item_group": "Products",
133 |     "stock_uom": "Nos"
134 |   }
135 | }
136 | </arguments>
137 | </use_mcp_tool>
138 | ```
139 | 
140 | ### Get Item Fields
141 | ```
142 | <use_mcp_tool>
143 | <server_name>erpnext</server_name>
144 | <tool_name>get_doctype_fields</tool_name>
145 | <arguments>
146 | {
147 |   "doctype": "Item"
148 | }
149 | </arguments>
150 | </use_mcp_tool>
151 | 
```

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

```markdown
 1 | # ERPNext MCP Server Improvement Documentation
 2 | 
 3 | This directory contains comprehensive documentation for improving the ERPNext MCP server implementation. These documents analyze the current state of the server and provide detailed recommendations for enhancing its maintainability, reliability, security, and functionality.
 4 | 
 5 | ## Document Overview
 6 | 
 7 | ### [Analysis](./analysis.md)
 8 | A comprehensive analysis of the current ERPNext MCP server implementation, identifying key areas for improvement including code organization, error handling, authentication, caching, testing, logging, documentation, configuration, rate limiting, tool implementation, security, resource management, and dependency management.
 9 | 
10 | ### [Code Restructuring](./code-restructuring.md)
11 | Detailed plan for restructuring the codebase to improve maintainability and testability, including a proposed directory structure, key component implementation examples, and step-by-step implementation plan.
12 | 
13 | ### [Testing Strategy](./testing-strategy.md)
14 | Comprehensive testing approach covering unit tests, integration tests, and end-to-end tests, with code examples, mock implementations, and configuration details for setting up a robust testing framework.
15 | 
16 | ### [Error Handling and Logging](./error-handling-and-logging.md)
17 | Strategy for implementing structured error handling and logging, including error categorization, correlation IDs, contextual logging, and integration with the MCP server.
18 | 
19 | ### [Security and Authentication](./security-and-authentication.md)
20 | Detailed recommendations for enhancing security and authentication, covering input validation, secure configuration management, rate limiting, security middleware, and best practices for credential handling.
21 | 
22 | ### [Implementation Roadmap](./implementation-roadmap.md)
23 | Phased approach for implementing all recommended improvements, with timeline estimates, dependencies, task prioritization, and risk management considerations.
24 | 
25 | ### [Feature Improvements](./feature-improvements.md)
26 | Detailed recommendations for enhancing the server's functionality with new features like advanced querying, bulk operations, webhooks, document workflows, file operations, and more.
27 | 
28 | ## Getting Started
29 | 
30 | If you're new to this documentation, we recommend starting with the [Analysis](./analysis.md) document to understand the current state and identified areas for improvement. Then, review the [Implementation Roadmap](./implementation-roadmap.md) for a structured approach to implementing the recommended changes.
31 | 
32 | ## Key Improvement Areas
33 | 
34 | 1. **Code Organization**: Transition from a monolithic design to a modular architecture
35 | 2. **Error Handling**: Implement structured error handling with proper categorization
36 | 3. **Testing**: Add comprehensive test coverage with unit, integration, and E2E tests
37 | 4. **Logging**: Enhance logging with structured formats and contextual information
38 | 5. **Security**: Improve authentication, add input validation, and implement rate limiting
39 | 6. **Performance**: Add caching and optimize API interactions
40 | 7. **Maintainability**: Improve documentation and implement best practices
41 | 
42 | ## Implementation Guide
43 | 
44 | For implementing these improvements, follow the phases outlined in the [Implementation Roadmap](./implementation-roadmap.md). This allows for incremental enhancement while maintaining a functioning system throughout the process.
45 | 
46 | ## Additional Resources
47 | 
48 | - [Model Context Protocol Documentation](https://github.com/modelcontextprotocol/mcp)
49 | - [ERPNext API Reference](https://frappeframework.com/docs/user/en/api)
50 | - [Frappe Framework Documentation](https://frappeframework.com/docs)
51 | 
```

--------------------------------------------------------------------------------
/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 | 
```

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

```json
 1 | {
 2 |   "name": "erpnext-server",
 3 |   "version": "0.1.0",
 4 |   "description": "A Model Context Protocol server",
 5 |   "private": true,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "erpnext-server": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 |     "prepare": "npm run build",
16 |     "watch": "tsc --watch",
17 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18 |   },
19 |   "dependencies": {
20 |     "@modelcontextprotocol/sdk": "0.6.0",
21 |     "axios": "^1.8.4"
22 |   },
23 |   "devDependencies": {
24 |     "@types/node": "^20.11.24",
25 |     "typescript": "^5.3.3"
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/docs/analysis.md:
--------------------------------------------------------------------------------

```markdown
  1 | # ERPNext MCP Server Analysis
  2 | 
  3 | This document provides an analysis of the current ERPNext MCP server implementation and suggests improvements for better reliability, maintainability, and extensibility.
  4 | 
  5 | ## Current Implementation Overview
  6 | 
  7 | The ERPNext MCP server currently provides integration with the ERPNext/Frappe API through:
  8 | 
  9 | - **Resource access**: Documents can be accessed via `erpnext://{doctype}/{name}` URIs
 10 | - **Tools**: Several tools for authentication, document manipulation, and report running
 11 | - **Single-file implementation**: All logic is contained in `src/index.ts`
 12 | 
 13 | ## Areas for Improvement
 14 | 
 15 | ### 1. Code Organization
 16 | 
 17 | **Issues:**
 18 | - The entire server implementation is in a single file, making it difficult to maintain as it grows
 19 | - No separation of concerns between API client, MCP server, and request handlers
 20 | - Mixed business logic and protocol handling
 21 | 
 22 | **Recommendations:**
 23 | - Restructure the codebase into multiple modules:
 24 |   ```
 25 |   src/
 26 |   ├── client/
 27 |   │   └── erpnext-client.ts           # ERPNext API client logic
 28 |   ├── handlers/
 29 |   │   ├── resource-handlers.ts        # Resource request handlers 
 30 |   │   └── tool-handlers.ts            # Tool request handlers
 31 |   ├── models/
 32 |   │   └── types.ts                    # TypeScript interfaces and types
 33 |   ├── utils/
 34 |   │   ├── error-handler.ts            # Error handling utilities
 35 |   │   ├── logger.ts                   # Logging utilities
 36 |   │   └── config.ts                   # Configuration management
 37 |   └── index.ts                        # Server bootstrap
 38 |   ```
 39 | 
 40 | ### 2. Error Handling
 41 | 
 42 | **Issues:**
 43 | - Basic error handling with limited categorization
 44 | - Inconsistent error response formats
 45 | - No handling of network-specific errors vs. business logic errors
 46 | 
 47 | **Recommendations:**
 48 | - Create a dedicated error handling module
 49 | - Map ERPNext API errors to appropriate MCP error codes
 50 | - Implement consistent error logging with appropriate detail levels
 51 | - Add correlation IDs to track errors across requests
 52 | 
 53 | ### 3. Authentication
 54 | 
 55 | **Issues:**
 56 | - Only username/password authentication supported
 57 | - No token refresh mechanism
 58 | - API key/secret handling is basic
 59 | 
 60 | **Recommendations:**
 61 | - Support modern OAuth-based authentication
 62 | - Implement token refresh strategy
 63 | - Add secure credential storage options
 64 | - Consider implementing session management
 65 | 
 66 | ### 4. Caching
 67 | 
 68 | **Issues:**
 69 | - `doctypeCache` is defined but never used
 70 | - No caching strategy for frequently accessed data
 71 | - Each request makes a fresh API call
 72 | 
 73 | **Recommendations:**
 74 | - Implement in-memory cache for DocType metadata
 75 | - Add time-based cache expiration
 76 | - Consider using a dedicated cache library for more complex scenarios
 77 | - Add cache invalidation when documents are updated
 78 | 
 79 | ### 5. Testing
 80 | 
 81 | **Issues:**
 82 | - No test suite
 83 | - No mocking of external dependencies
 84 | 
 85 | **Recommendations:**
 86 | - Add unit tests for core functionality
 87 | - Implement integration tests for ERPNext client
 88 | - Create mock ERPNext API for testing
 89 | - Set up CI/CD pipeline with test automation
 90 | 
 91 | ### 6. Logging
 92 | 
 93 | **Issues:**
 94 | - Basic console logging
 95 | - No structured log format
 96 | - No log levels for different environments
 97 | 
 98 | **Recommendations:**
 99 | - Implement structured logging
100 | - Add configurable log levels
101 | - Include request/response details for debugging
102 | - Consider using a dedicated logging library
103 | 
104 | ### 7. Documentation
105 | 
106 | **Issues:**
107 | - Basic README with limited examples
108 | - No JSDoc comments for most functions
109 | - No detailed API documentation
110 | 
111 | **Recommendations:**
112 | - Add comprehensive JSDoc comments
113 | - Create detailed API reference
114 | - Add code examples for common scenarios
115 | - Document error codes and their meanings
116 | - Consider generating API docs with TypeDoc
117 | 
118 | ### 8. Configuration
119 | 
120 | **Issues:**
121 | - Environment variables handled in an ad-hoc way
122 | - No configuration validation
123 | - Limited configuration options
124 | 
125 | **Recommendations:**
126 | - Create a dedicated configuration module
127 | - Add validation for required configuration
128 | - Support multiple configuration sources (env vars, config files)
129 | - Add configuration schema validation
130 | 
131 | ### 9. Rate Limiting
132 | 
133 | **Issues:**
134 | - No protection against excessive API calls
135 | - Potential for unintended DoS on ERPNext instance
136 | 
137 | **Recommendations:**
138 | - Implement rate limiting for API calls
139 | - Add configurable limits per operation
140 | - Provide feedback on rate limit status
141 | 
142 | ### 10. Tool Implementation
143 | 
144 | **Issues:**
145 | - `get_doctype_fields` has a hacky implementation that loads a sample document
146 | - Limited error handling in tool implementations
147 | - No pagination support for large result sets
148 | 
149 | **Recommendations:**
150 | - Use proper metadata APIs to get DocType fields
151 | - Implement pagination for list operations
152 | - Add better validation of tool inputs
153 | - Consider adding additional useful tools (e.g., bulk operations)
154 | 
155 | ### 11. Security
156 | 
157 | **Issues:**
158 | - Passwords are passed directly with no handling recommendations
159 | - No security headers or CORS configuration
160 | - No input sanitization
161 | 
162 | **Recommendations:**
163 | - Add input validation and sanitization
164 | - Improve credential handling and provide secure usage guidelines
165 | - Document security best practices
166 | 
167 | ### 12. Resource Management
168 | 
169 | **Issues:**
170 | - Limited resource implementations compared to tools
171 | - No dynamic resource discovery
172 | 
173 | **Recommendations:**
174 | - Expand resource capabilities
175 | - Implement more resource templates
176 | - Add resource discovery mechanism
177 | 
178 | ### 13. Dependency Management
179 | 
180 | **Issues:**
181 | - No dependency injection for better testability
182 | - Direct dependency on axios without abstraction
183 | 
184 | **Recommendations:**
185 | - Implement dependency injection pattern
186 | - Create HTTP client abstraction to allow switching libraries
187 | - Centralize external dependency management
188 | 
189 | ## Conclusion
190 | 
191 | The ERPNext MCP server provides a solid foundation for integration with ERPNext but could benefit from several improvements to make it more maintainable, robust, and feature-rich. The recommendations provided above would help transform it into a production-ready solution that's easier to maintain and extend.
192 | 
```

--------------------------------------------------------------------------------
/docs/implementation-roadmap.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Implementation Roadmap
  2 | 
  3 | This document outlines a comprehensive roadmap for implementing the improvements suggested in the analysis of the ERPNext MCP server.
  4 | 
  5 | ## Overview
  6 | 
  7 | The ERPNext MCP server provides integration with ERPNext through the Model Context Protocol, but several improvements can enhance its maintainability, reliability, and security. This roadmap prioritizes these improvements into phases for systematic implementation.
  8 | 
  9 | ## Phase 1: Code Restructuring and Basic Improvements
 10 | 
 11 | **Objective**: Establish a solid foundation by restructuring the codebase into a more maintainable architecture.
 12 | 
 13 | **Duration**: 2-3 weeks
 14 | 
 15 | ### Tasks
 16 | 
 17 | 1. **Project Structure Reorganization**
 18 |    - Create directory structure as outlined in [`code-restructuring.md`](./code-restructuring.md)
 19 |    - Split the monolithic `src/index.ts` into modular components
 20 | 
 21 | 2. **Core Module Extraction**
 22 |    - Extract ERPNext client into `src/client/erpnext-client.ts`
 23 |    - Extract resource handlers into `src/handlers/resource-handlers.ts`
 24 |    - Extract tool handlers into `src/handlers/tool-handlers.ts`
 25 | 
 26 | 3. **Configuration Management**
 27 |    - Create dedicated configuration module in `src/utils/config.ts`
 28 |    - Implement validation for required configuration
 29 |    - Add support for multiple configuration sources
 30 | 
 31 | 4. **Basic Logging**
 32 |    - Implement structured logging in `src/utils/logger.ts`
 33 |    - Add different log levels for development and production
 34 |    - Add contextual information to logs
 35 | 
 36 | ### Deliverables
 37 | - Modular codebase with clear separation of concerns
 38 | - Improved configuration management
 39 | - Basic structured logging
 40 | 
 41 | ## Phase 2: Enhanced Error Handling and Testing
 42 | 
 43 | **Objective**: Improve reliability through better error handling and establish a testing framework.
 44 | 
 45 | **Duration**: 3-4 weeks
 46 | 
 47 | ### Tasks
 48 | 
 49 | 1. **Error Handling**
 50 |    - Implement error categorization as outlined in [`error-handling-and-logging.md`](./error-handling-and-logging.md)
 51 |    - Create error mapping between ERPNext and MCP error codes
 52 |    - Add correlation IDs for tracking errors across the system
 53 | 
 54 | 2. **Testing Framework Setup**
 55 |    - Set up Jest for testing
 56 |    - Create test directory structure
 57 |    - Add test utilities and mocks
 58 | 
 59 | 3. **Unit Tests**
 60 |    - Write unit tests for core modules (client, utils)
 61 |    - Achieve at least 70% code coverage
 62 | 
 63 | 4. **Integration Tests**
 64 |    - Add integration tests for API interactions
 65 |    - Create mock ERPNext API for testing
 66 | 
 67 | ### Deliverables
 68 | - Robust error handling system
 69 | - Test suite with good coverage
 70 | - Mock ERPNext API for testing
 71 | 
 72 | ## Phase 3: Security and Authentication Enhancements
 73 | 
 74 | **Objective**: Strengthen security and improve authentication mechanisms.
 75 | 
 76 | **Duration**: 2-3 weeks
 77 | 
 78 | ### Tasks
 79 | 
 80 | 1. **Input Validation**
 81 |    - Create validation module as outlined in [`security-and-authentication.md`](./security-and-authentication.md)
 82 |    - Implement schema validation for all inputs
 83 |    - Add input sanitization to prevent injection attacks
 84 | 
 85 | 2. **Authentication Improvements**
 86 |    - Implement authentication manager
 87 |    - Add token management
 88 |    - Implement secure credential handling
 89 | 
 90 | 3. **Rate Limiting**
 91 |    - Add rate limiting middleware
 92 |    - Implement different limits for different operations
 93 |    - Add protection against brute force attacks
 94 | 
 95 | 4. **Security Headers**
 96 |    - Add security headers to responses
 97 |    - Implement secure defaults
 98 | 
 99 | ### Deliverables
100 | - Secure input validation
101 | - Enhanced authentication
102 | - Protection against common attacks
103 | 
104 | ## Phase 4: Advanced Features and Performance
105 | 
106 | **Objective**: Add advanced features and optimize performance.
107 | 
108 | **Duration**: 4-6 weeks
109 | 
110 | ### Tasks
111 | 
112 | 1. **Caching Implementation**
113 |    - Implement in-memory cache
114 |    - Add cache invalidation logic
115 |    - Optimize frequently accessed resources
116 | 
117 | 2. **Documentation**
118 |    - Add comprehensive JSDoc comments
119 |    - Generate API documentation
120 |    - Update README with examples
121 | 
122 | 3. **Pagination Support**
123 |    - Add pagination for list operations
124 |    - Implement cursor-based pagination for large result sets
125 | 
126 | 4. **Resource Expansion**
127 |    - Enhance resource capabilities
128 |    - Add more resource templates
129 |    - Improve discovery mechanism
130 | 
131 | 5. **Performance Optimization**
132 |    - Implement request batching
133 |    - Optimize network requests
134 |    - Add performance metrics
135 | 
136 | 6. **Feature Enhancements** (as outlined in [`feature-improvements.md`](./feature-improvements.md))
137 |    - Implement enhanced DocType discovery and metadata
138 |    - Add advanced querying capabilities
139 |    - Develop bulk operations support
140 |    - Create file operations functionality
141 |    - Add webhook and event support
142 |    - Implement document workflows
143 |    - Develop data synchronization utilities
144 | 
145 | ### Deliverables
146 | - Caching system for improved performance
147 | - Comprehensive documentation
148 | - Enhanced resource capabilities
149 | - Performance metrics and optimizations
150 | - New functional capabilities as detailed in feature improvements
151 | 
152 | ## Phase 5: CI/CD and DevOps
153 | 
154 | **Objective**: Set up continuous integration and deployment pipeline.
155 | 
156 | **Duration**: 1-2 weeks
157 | 
158 | ### Tasks
159 | 
160 | 1. **CI Setup**
161 |    - Configure GitHub Actions or similar CI platform
162 |    - Automate testing
163 |    - Implement code quality checks
164 | 
165 | 2. **Automated Builds**
166 |    - Set up automated builds
167 |    - Create release versioning
168 |    - Generate build artifacts
169 | 
170 | 3. **Deployment Automation**
171 |    - Create deployment scripts
172 |    - Add environment configuration templates
173 |    - Document deployment process
174 | 
175 | ### Deliverables
176 | - Automated CI/CD pipeline
177 | - Code quality checks
178 | - Streamlined release process
179 | 
180 | ## Timeline Overview
181 | 
182 | ```
183 | Week 1-3:   Phase 1 - Code Restructuring
184 | Week 4-7:   Phase 2 - Error Handling and Testing
185 | Week 8-10:  Phase 3 - Security Enhancements
186 | Week 11-14: Phase 4 - Advanced Features
187 | Week 15-16: Phase 5 - CI/CD and DevOps
188 | ```
189 | 
190 | ## Implementation Priorities
191 | 
192 | When implementing these improvements, follow these priorities:
193 | 
194 | 1. **Critical**: Code restructuring, basic error handling
195 | 2. **High**: Testing setup, security improvements
196 | 3. **Medium**: Caching, pagination, documentation
197 | 4. **Low**: Advanced features, CI/CD
198 | 
199 | ## Dependencies and Requirements
200 | 
201 | For implementing these improvements, ensure:
202 | 
203 | 1. Access to ERPNext instance for testing
204 | 2. Node.js development environment
205 | 3. Knowledge of TypeScript and MCP SDK
206 | 4. Understanding of ERPNext API
207 | 
208 | ## Risk Management
209 | 
210 | Some potential risks to consider:
211 | 
212 | 1. **API Changes**: ERPNext may update its API, requiring adjustments
213 | 2. **Backward Compatibility**: Ensure changes don't break existing clients
214 | 3. **Performance Impact**: Monitor performance impacts of changes
215 | 
216 | ## Conclusion
217 | 
218 | Following this roadmap will transform the ERPNext MCP server into a robust, maintainable, and secure system. The phased approach allows for incremental improvements while maintaining a functioning system throughout the process.
219 | 
```

--------------------------------------------------------------------------------
/docs/code-restructuring.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Code Restructuring Plan
  2 | 
  3 | This document outlines a plan for restructuring the ERPNext MCP server codebase to improve maintainability, testability, and extensibility.
  4 | 
  5 | ## Current Structure
  6 | 
  7 | Currently, the entire implementation is in a single file (`src/index.ts`), which contains:
  8 | 
  9 | - ERPNext API client
 10 | - MCP server setup
 11 | - Resource handlers
 12 | - Tool handlers
 13 | - Error handling
 14 | - Authentication logic
 15 | 
 16 | This monolithic approach makes the code difficult to maintain, test, and extend.
 17 | 
 18 | ## Proposed Structure
 19 | 
 20 | ### Directory Structure
 21 | 
 22 | ```
 23 | src/
 24 | ├── client/
 25 | │   └── erpnext-client.ts          # ERPNext API client
 26 | ├── handlers/
 27 | │   ├── resource-handlers.ts       # Resource request handlers
 28 | │   └── tool-handlers.ts           # Tool request handlers
 29 | ├── models/
 30 | │   ├── doctype.ts                 # DocType interfaces
 31 | │   └── errors.ts                  # Error types
 32 | ├── utils/
 33 | │   ├── cache.ts                   # Caching utilities
 34 | │   ├── config.ts                  # Configuration management
 35 | │   ├── error-handler.ts           # Error handling utilities
 36 | │   └── logger.ts                  # Logging utilities
 37 | ├── constants/
 38 | │   └── error-codes.ts             # Error code definitions
 39 | ├── middleware/
 40 | │   ├── auth.ts                    # Authentication middleware
 41 | │   └── rate-limiter.ts            # Rate limiting middleware
 42 | └── index.ts                       # Server bootstrap
 43 | ```
 44 | 
 45 | ### Key Components
 46 | 
 47 | #### 1. ERPNext Client (`src/client/erpnext-client.ts`)
 48 | 
 49 | Extract the ERPNext API client into a dedicated module:
 50 | 
 51 | ```typescript
 52 | // src/client/erpnext-client.ts
 53 | import axios, { AxiosInstance } from "axios";
 54 | import { Logger } from "../utils/logger";
 55 | import { Config } from "../utils/config";
 56 | import { ERPNextError } from "../models/errors";
 57 | 
 58 | export class ERPNextClient {
 59 |   private baseUrl: string;
 60 |   private axiosInstance: AxiosInstance;
 61 |   private authenticated: boolean = false;
 62 |   private logger: Logger;
 63 | 
 64 |   constructor(config: Config, logger: Logger) {
 65 |     this.logger = logger;
 66 |     this.baseUrl = config.getERPNextUrl();
 67 |     
 68 |     // Initialize axios instance
 69 |     this.axiosInstance = axios.create({
 70 |       baseURL: this.baseUrl,
 71 |       withCredentials: true,
 72 |       headers: {
 73 |         'Content-Type': 'application/json',
 74 |         'Accept': 'application/json'
 75 |       }
 76 |     });
 77 |     
 78 |     // Configure authentication if credentials provided
 79 |     const apiKey = config.getERPNextApiKey();
 80 |     const apiSecret = config.getERPNextApiSecret();
 81 |     
 82 |     if (apiKey && apiSecret) {
 83 |       this.axiosInstance.defaults.headers.common['Authorization'] = 
 84 |         `token ${apiKey}:${apiSecret}`;
 85 |       this.authenticated = true;
 86 |       this.logger.info("Initialized with API key authentication");
 87 |     }
 88 |   }
 89 | 
 90 |   // Methods for API operations...
 91 | }
 92 | ```
 93 | 
 94 | #### 2. Resource Handlers (`src/handlers/resource-handlers.ts`)
 95 | 
 96 | Move the resource-related request handlers to a dedicated module:
 97 | 
 98 | ```typescript
 99 | // src/handlers/resource-handlers.ts
100 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
101 | import {
102 |   ListResourcesRequestSchema,
103 |   ListResourceTemplatesRequestSchema,
104 |   ReadResourceRequestSchema,
105 |   McpError,
106 |   ErrorCode
107 | } from "@modelcontextprotocol/sdk/types.js";
108 | import { ERPNextClient } from "../client/erpnext-client";
109 | import { Logger } from "../utils/logger";
110 | import { Cache } from "../utils/cache";
111 | 
112 | export function registerResourceHandlers(
113 |   server: Server, 
114 |   erpnext: ERPNextClient,
115 |   cache: Cache,
116 |   logger: Logger
117 | ) {
118 |   // Handler for listing resources
119 |   server.setRequestHandler(ListResourcesRequestSchema, async () => {
120 |     logger.debug("Handling ListResourcesRequest");
121 |     
122 |     const resources = [
123 |       {
124 |         uri: "erpnext://DocTypes",
125 |         name: "All DocTypes",
126 |         mimeType: "application/json",
127 |         description: "List of all available DocTypes in the ERPNext instance"
128 |       }
129 |     ];
130 | 
131 |     return { resources };
132 |   });
133 | 
134 |   // Other resource handlers...
135 | }
136 | ```
137 | 
138 | #### 3. Tool Handlers (`src/handlers/tool-handlers.ts`)
139 | 
140 | Similarly, move the tool-related request handlers:
141 | 
142 | ```typescript
143 | // src/handlers/tool-handlers.ts
144 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
145 | import {
146 |   CallToolRequestSchema,
147 |   ListToolsRequestSchema,
148 |   McpError,
149 |   ErrorCode
150 | } from "@modelcontextprotocol/sdk/types.js";
151 | import { ERPNextClient } from "../client/erpnext-client";
152 | import { Logger } from "../utils/logger";
153 | import { handleErrors } from "../utils/error-handler";
154 | 
155 | export function registerToolHandlers(
156 |   server: Server, 
157 |   erpnext: ERPNextClient,
158 |   logger: Logger
159 | ) {
160 |   // Handler for listing tools
161 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
162 |     logger.debug("Handling ListToolsRequest");
163 |     
164 |     return {
165 |       tools: [
166 |         {
167 |           name: "get_doctypes",
168 |           description: "Get a list of all available DocTypes",
169 |           inputSchema: {
170 |             type: "object",
171 |             properties: {}
172 |           }
173 |         },
174 |         // Other tools...
175 |       ]
176 |     };
177 |   });
178 | 
179 |   // Handler for tool calls with proper error handling
180 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
181 |     logger.debug(`Handling CallToolRequest: ${request.params.name}`);
182 |     
183 |     try {
184 |       switch (request.params.name) {
185 |         case "authenticate_erpnext":
186 |           return await handleAuthenticateErpnext(request, erpnext, logger);
187 |         // Other tool handlers...
188 |         default:
189 |           throw new McpError(
190 |             ErrorCode.MethodNotFound,
191 |             `Unknown tool: ${request.params.name}`
192 |           );
193 |       }
194 |     } catch (error) {
195 |       return handleErrors(error, logger);
196 |     }
197 |   });
198 | }
199 | 
200 | // Individual tool handler functions
201 | async function handleAuthenticateErpnext(request, erpnext, logger) {
202 |   // Implementation...
203 | }
204 | ```
205 | 
206 | #### 4. Cache Utility (`src/utils/cache.ts`)
207 | 
208 | Implement the previously defined but unused cache:
209 | 
210 | ```typescript
211 | // src/utils/cache.ts
212 | export class Cache {
213 |   private cache: Map<string, CacheEntry>;
214 |   private defaultTTLMs: number;
215 |   
216 |   constructor(defaultTTLMs: number = 5 * 60 * 1000) { // 5 minutes default
217 |     this.cache = new Map();
218 |     this.defaultTTLMs = defaultTTLMs;
219 |   }
220 |   
221 |   set(key: string, value: any, ttlMs?: number): void {
222 |     const expiryTime = Date.now() + (ttlMs || this.defaultTTLMs);
223 |     this.cache.set(key, { value, expiryTime });
224 |   }
225 |   
226 |   get<T>(key: string): T | undefined {
227 |     const entry = this.cache.get(key);
228 |     
229 |     if (!entry) {
230 |       return undefined;
231 |     }
232 |     
233 |     if (Date.now() > entry.expiryTime) {
234 |       this.cache.delete(key);
235 |       return undefined;
236 |     }
237 |     
238 |     return entry.value as T;
239 |   }
240 |   
241 |   invalidate(keyPrefix: string): void {
242 |     for (const key of this.cache.keys()) {
243 |       if (key.startsWith(keyPrefix)) {
244 |         this.cache.delete(key);
245 |       }
246 |     }
247 |   }
248 | }
249 | 
250 | interface CacheEntry {
251 |   value: any;
252 |   expiryTime: number;
253 | }
254 | ```
255 | 
256 | #### 5. Configuration Module (`src/utils/config.ts`)
257 | 
258 | Create a dedicated configuration module:
259 | 
260 | ```typescript
261 | // src/utils/config.ts
262 | export class Config {
263 |   private erpnextUrl: string;
264 |   private apiKey?: string;
265 |   private apiSecret?: string;
266 |   
267 |   constructor() {
268 |     this.erpnextUrl = this.getRequiredEnv("ERPNEXT_URL");
269 |     // Remove trailing slash if present
270 |     this.erpnextUrl = this.erpnextUrl.replace(/\/$/, '');
271 |     
272 |     this.apiKey = process.env.ERPNEXT_API_KEY;
273 |     this.apiSecret = process.env.ERPNEXT_API_SECRET;
274 |     
275 |     this.validate();
276 |   }
277 |   
278 |   private getRequiredEnv(name: string): string {
279 |     const value = process.env[name];
280 |     if (!value) {
281 |       throw new Error(`${name} environment variable is required`);
282 |     }
283 |     return value;
284 |   }
285 |   
286 |   private validate() {
287 |     if (!this.erpnextUrl.startsWith("http")) {
288 |       throw new Error("ERPNEXT_URL must include protocol (http:// or https://)");
289 |     }
290 |     
291 |     // If one of API key/secret is provided, both must be provided
292 |     if ((this.apiKey && !this.apiSecret) || (!this.apiKey && this.apiSecret)) {
293 |       throw new Error("Both ERPNEXT_API_KEY and ERPNEXT_API_SECRET must be provided if using API key authentication");
294 |     }
295 |   }
296 |   
297 |   getERPNextUrl(): string {
298 |     return this.erpnextUrl;
299 |   }
300 |   
301 |   getERPNextApiKey(): string | undefined {
302 |     return this.apiKey;
303 |   }
304 |   
305 |   getERPNextApiSecret(): string | undefined {
306 |     return this.apiSecret;
307 |   }
308 | }
309 | ```
310 | 
311 | #### 6. Logger Module (`src/utils/logger.ts`)
312 | 
313 | Implement a proper logging utility:
314 | 
315 | ```typescript
316 | // src/utils/logger.ts
317 | export enum LogLevel {
318 |   ERROR = 0,
319 |   WARN = 1,
320 |   INFO = 2,
321 |   DEBUG = 3,
322 | }
323 | 
324 | export class Logger {
325 |   private level: LogLevel;
326 |   
327 |   constructor(level: LogLevel = LogLevel.INFO) {
328 |     this.level = level;
329 |   }
330 |   
331 |   setLevel(level: LogLevel) {
332 |     this.level = level;
333 |   }
334 |   
335 |   error(message: string, ...meta: any[]) {
336 |     if (this.level >= LogLevel.ERROR) {
337 |       console.error(`[ERROR] ${message}`, ...meta);
338 |     }
339 |   }
340 |   
341 |   warn(message: string, ...meta: any[]) {
342 |     if (this.level >= LogLevel.WARN) {
343 |       console.warn(`[WARN] ${message}`, ...meta);
344 |     }
345 |   }
346 |   
347 |   info(message: string, ...meta: any[]) {
348 |     if (this.level >= LogLevel.INFO) {
349 |       console.info(`[INFO] ${message}`, ...meta);
350 |     }
351 |   }
352 |   
353 |   debug(message: string, ...meta: any[]) {
354 |     if (this.level >= LogLevel.DEBUG) {
355 |       console.debug(`[DEBUG] ${message}`, ...meta);
356 |     }
357 |   }
358 | }
359 | ```
360 | 
361 | #### 7. Main File (`src/index.ts`)
362 | 
363 | The main file becomes much simpler:
364 | 
365 | ```typescript
366 | #!/usr/bin/env node
367 | 
368 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
369 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
370 | import { Config } from "./utils/config";
371 | import { Logger, LogLevel } from "./utils/logger";
372 | import { Cache } from "./utils/cache";
373 | import { ERPNextClient } from "./client/erpnext-client";
374 | import { registerResourceHandlers } from "./handlers/resource-handlers";
375 | import { registerToolHandlers } from "./handlers/tool-handlers";
376 | 
377 | async function main() {
378 |   // Initialize components
379 |   const logger = new Logger(
380 |     process.env.DEBUG ? LogLevel.DEBUG : LogLevel.INFO
381 |   );
382 |   
383 |   try {
384 |     logger.info("Initializing ERPNext MCP server");
385 |     
386 |     const config = new Config();
387 |     const cache = new Cache();
388 |     const erpnext = new ERPNextClient(config, logger);
389 |     
390 |     // Create server
391 |     const server = new Server(
392 |       {
393 |         name: "erpnext-server",
394 |         version: "0.1.0"
395 |       },
396 |       {
397 |         capabilities: {
398 |           resources: {},
399 |           tools: {}
400 |         }
401 |       }
402 |     );
403 |     
404 |     // Register handlers
405 |     registerResourceHandlers(server, erpnext, cache, logger);
406 |     registerToolHandlers(server, erpnext, logger);
407 |     
408 |     // Setup error handling
409 |     server.onerror = (error) => {
410 |       logger.error("Server error:", error);
411 |     };
412 |     
413 |     // Start server
414 |     const transport = new StdioServerTransport();
415 |     await server.connect(transport);
416 |     logger.info('ERPNext MCP server running on stdio');
417 |     
418 |     // Handle graceful shutdown
419 |     process.on('SIGINT', async () => {
420 |       logger.info("Shutting down...");
421 |       await server.close();
422 |       process.exit(0);
423 |     });
424 |   } catch (error) {
425 |     logger.error("Failed to start server:", error);
426 |     process.exit(1);
427 |   }
428 | }
429 | 
430 | main();
431 | ```
432 | 
433 | ## Implementation Plan
434 | 
435 | 1. Create the directory structure
436 | 2. Move the ERPNext client to its own module
437 | 3. Create utility modules (config, logger, cache)
438 | 4. Split out the resource and tool handlers
439 | 5. Update the main file to use the new modules
440 | 6. Add tests for each module
441 | 7. Update documentation
442 | 
443 | This restructuring will make the code more maintainable, easier to test, and facilitate future enhancements.
444 | 
```

--------------------------------------------------------------------------------
/docs/feature-improvements.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Feature Improvements
  2 | 
  3 | This document outlines potential feature improvements for the ERPNext MCP server to enhance its functionality, usability, and integration capabilities with ERPNext.
  4 | 
  5 | ## Current Feature Set
  6 | 
  7 | Currently, the ERPNext MCP server provides these core features:
  8 | 
  9 | 1. **Authentication** with ERPNext using username/password or API keys
 10 | 2. **Document Operations** (get, list, create, update)
 11 | 3. **Report Running**
 12 | 4. **DocType Information** (listing DocTypes and fields)
 13 | 5. **Basic Resource Access** via URIs
 14 | 
 15 | ## Proposed Feature Improvements
 16 | 
 17 | ### 1. Enhanced DocType Discovery and Metadata
 18 | 
 19 | **Current Limitation**: Basic DocType listing without metadata or relationships.
 20 | 
 21 | **Proposed Improvements**:
 22 | - Add metadata about each DocType (description, icon, module)
 23 | - Include field type information and validations
 24 | - Show relationships between DocTypes (links, child tables)
 25 | - Provide DocType-specific operations and permissions
 26 | 
 27 | **Implementation**:
 28 | ```typescript
 29 | // src/handlers/tool-handlers.ts
 30 | // New tool: get_doctype_metadata
 31 | 
 32 | export async function handleGetDoctypeMetadata(request, erpnext, logger) {
 33 |   const doctype = request.params.arguments?.doctype;
 34 |   
 35 |   if (!doctype) {
 36 |     throw new McpError(ErrorCode.InvalidParams, "DocType is required");
 37 |   }
 38 |   
 39 |   try {
 40 |     // Get DocType metadata including fields with types
 41 |     const metadata = await erpnext.getDocTypeMetadata(doctype);
 42 |     
 43 |     // Get relationship information
 44 |     const relationships = await erpnext.getDocTypeRelationships(doctype);
 45 |     
 46 |     return {
 47 |       content: [{
 48 |         type: "text",
 49 |         text: JSON.stringify({
 50 |           doctype,
 51 |           metadata,
 52 |           relationships,
 53 |           operations: getAvailableOperations(doctype)
 54 |         }, null, 2)
 55 |       }]
 56 |     };
 57 |   } catch (error) {
 58 |     return handleToolError(error, logger);
 59 |   }
 60 | }
 61 | ```
 62 | 
 63 | ### 2. Bulk Operations
 64 | 
 65 | **Current Limitation**: Operations are limited to single documents.
 66 | 
 67 | **Proposed Improvements**:
 68 | - Add bulk document creation
 69 | - Implement batch updates
 70 | - Support bulk data import/export
 71 | - Add atomic transaction support
 72 | 
 73 | **Implementation**:
 74 | ```typescript
 75 | // New tool: bulk_create_documents
 76 | 
 77 | export async function handleBulkCreateDocuments(request, erpnext, logger) {
 78 |   const doctype = request.params.arguments?.doctype;
 79 |   const documents = request.params.arguments?.documents;
 80 |   
 81 |   if (!doctype || !Array.isArray(documents)) {
 82 |     throw new McpError(ErrorCode.InvalidParams, "DocType and array of documents are required");
 83 |   }
 84 |   
 85 |   try {
 86 |     // Create documents in a transaction if possible
 87 |     const results = await erpnext.bulkCreateDocuments(doctype, documents);
 88 |     
 89 |     return {
 90 |       content: [{
 91 |         type: "text",
 92 |         text: JSON.stringify({
 93 |           message: `Created ${results.length} ${doctype} documents`,
 94 |           results
 95 |         }, null, 2)
 96 |       }]
 97 |     };
 98 |   } catch (error) {
 99 |     return handleToolError(error, logger);
100 |   }
101 | }
102 | ```
103 | 
104 | ### 3. Advanced Querying
105 | 
106 | **Current Limitation**: Basic filtering without complex queries.
107 | 
108 | **Proposed Improvements**:
109 | - Support complex query conditions
110 | - Add sorting and grouping options
111 | - Implement flexible field selection
112 | - Add query templates for common operations
113 | 
114 | **Implementation**:
115 | ```typescript
116 | // New tool: advanced_query
117 | 
118 | export async function handleAdvancedQuery(request, erpnext, logger) {
119 |   const doctype = request.params.arguments?.doctype;
120 |   const query = request.params.arguments?.query || {};
121 |   
122 |   if (!doctype) {
123 |     throw new McpError(ErrorCode.InvalidParams, "DocType is required");
124 |   }
125 |   
126 |   try {
127 |     // Transform query to ERPNext format
128 |     const erpnextQuery = transformQuery(query);
129 |     
130 |     // Execute query
131 |     const results = await erpnext.advancedQuery(doctype, erpnextQuery);
132 |     
133 |     return {
134 |       content: [{
135 |         type: "text",
136 |         text: JSON.stringify(results, null, 2)
137 |       }]
138 |     };
139 |   } catch (error) {
140 |     return handleToolError(error, logger);
141 |   }
142 | }
143 | 
144 | function transformQuery(query) {
145 |   // Transform from MCP query format to ERPNext query format
146 |   const result = {
147 |     filters: query.filters || {},
148 |     fields: query.fields || ["*"],
149 |     limit: query.limit,
150 |     offset: query.offset,
151 |     order_by: query.orderBy,
152 |     group_by: query.groupBy
153 |   };
154 |   
155 |   // Handle complex conditions
156 |   if (query.conditions) {
157 |     result.filters = buildComplexFilters(query.conditions);
158 |   }
159 |   
160 |   return result;
161 | }
162 | ```
163 | 
164 | ### 4. Webhooks and Events
165 | 
166 | **Current Limitation**: No support for event-driven operations.
167 | 
168 | **Proposed Improvements**:
169 | - Add webhook registration for ERPNext events
170 | - Implement event listeners
171 | - Support callback URLs for async operations
172 | - Create notification tools
173 | 
174 | **Implementation**:
175 | ```typescript
176 | // New tool: register_webhook
177 | 
178 | export async function handleRegisterWebhook(request, erpnext, logger) {
179 |   const doctype = request.params.arguments?.doctype;
180 |   const event = request.params.arguments?.event;
181 |   const callbackUrl = request.params.arguments?.callbackUrl;
182 |   
183 |   if (!doctype || !event || !callbackUrl) {
184 |     throw new McpError(ErrorCode.InvalidParams, "DocType, event, and callbackUrl are required");
185 |   }
186 |   
187 |   try {
188 |     // Register webhook with ERPNext
189 |     const webhook = await erpnext.registerWebhook(doctype, event, callbackUrl);
190 |     
191 |     return {
192 |       content: [{
193 |         type: "text",
194 |         text: JSON.stringify({
195 |           message: `Webhook registered for ${doctype} ${event} events`,
196 |           webhook
197 |         }, null, 2)
198 |       }]
199 |     };
200 |   } catch (error) {
201 |     return handleToolError(error, logger);
202 |   }
203 | }
204 | ```
205 | 
206 | ### 5. Document Workflows
207 | 
208 | **Current Limitation**: No workflow or process automation.
209 | 
210 | **Proposed Improvements**:
211 | - Add workflow status tracking
212 | - Implement state transitions
213 | - Support approval processes
214 | - Create multi-step operations
215 | 
216 | **Implementation**:
217 | ```typescript
218 | // New tool: execute_workflow_action
219 | 
220 | export async function handleExecuteWorkflowAction(request, erpnext, logger) {
221 |   const doctype = request.params.arguments?.doctype;
222 |   const name = request.params.arguments?.name;
223 |   const action = request.params.arguments?.action;
224 |   const comments = request.params.arguments?.comments;
225 |   
226 |   if (!doctype || !name || !action) {
227 |     throw new McpError(ErrorCode.InvalidParams, "DocType, document name, and action are required");
228 |   }
229 |   
230 |   try {
231 |     // Execute workflow action
232 |     const result = await erpnext.executeWorkflowAction(doctype, name, action, comments);
233 |     
234 |     return {
235 |       content: [{
236 |         type: "text",
237 |         text: JSON.stringify({
238 |           message: `Executed ${action} on ${doctype} ${name}`,
239 |           result
240 |         }, null, 2)
241 |       }]
242 |     };
243 |   } catch (error) {
244 |     return handleToolError(error, logger);
245 |   }
246 | }
247 | ```
248 | 
249 | ### 6. File Operations
250 | 
251 | **Current Limitation**: No file handling capabilities.
252 | 
253 | **Proposed Improvements**:
254 | - Add file upload/download
255 | - Support attachments to documents
256 | - Implement file metadata management
257 | - Add image processing utilities
258 | 
259 | **Implementation**:
260 | ```typescript
261 | // New tool: upload_attachment
262 | 
263 | export async function handleUploadAttachment(request, erpnext, logger) {
264 |   const doctype = request.params.arguments?.doctype;
265 |   const name = request.params.arguments?.name;
266 |   const fileName = request.params.arguments?.fileName;
267 |   const fileContent = request.params.arguments?.fileContent; // Base64 encoded
268 |   const fileType = request.params.arguments?.fileType;
269 |   
270 |   if (!doctype || !name || !fileName || !fileContent) {
271 |     throw new McpError(ErrorCode.InvalidParams, "DocType, document name, fileName, and fileContent are required");
272 |   }
273 |   
274 |   try {
275 |     // Upload attachment
276 |     const attachment = await erpnext.uploadAttachment(
277 |       doctype, 
278 |       name, 
279 |       fileName, 
280 |       Buffer.from(fileContent, 'base64'), 
281 |       fileType
282 |     );
283 |     
284 |     return {
285 |       content: [{
286 |         type: "text",
287 |         text: JSON.stringify({
288 |           message: `Attached ${fileName} to ${doctype} ${name}`,
289 |           attachment
290 |         }, null, 2)
291 |       }]
292 |     };
293 |   } catch (error) {
294 |     return handleToolError(error, logger);
295 |   }
296 | }
297 | ```
298 | 
299 | ### 7. Data Synchronization
300 | 
301 | **Current Limitation**: No synchronization utilities.
302 | 
303 | **Proposed Improvements**:
304 | - Add data synchronization capabilities
305 | - Implement change tracking
306 | - Support incremental updates
307 | - Create data migration tools
308 | 
309 | **Implementation**:
310 | ```typescript
311 | // New tool: sync_data
312 | 
313 | export async function handleSyncData(request, erpnext, logger) {
314 |   const doctype = request.params.arguments?.doctype;
315 |   const lastSyncTime = request.params.arguments?.lastSyncTime;
316 |   
317 |   if (!doctype) {
318 |     throw new McpError(ErrorCode.InvalidParams, "DocType is required");
319 |   }
320 |   
321 |   try {
322 |     // Get changes since last sync
323 |     const changes = await erpnext.getChangesSince(doctype, lastSyncTime);
324 |     
325 |     return {
326 |       content: [{
327 |         type: "text",
328 |         text: JSON.stringify({
329 |         message: `Retrieved ${changes.length} changes for ${doctype} since ${lastSyncTime || 'beginning'}`,
330 |           currentTime: new Date().toISOString(),
331 |           changes
332 |         }, null, 2)
333 |       }]
334 |     };
335 |   } catch (error) {
336 |     return handleToolError(error, logger);
337 |   }
338 | }
339 | ```
340 | 
341 | ### 8. Custom Scripts and Server Actions
342 | 
343 | **Current Limitation**: No support for custom scripts or actions.
344 | 
345 | **Proposed Improvements**:
346 | - Add support for executing custom server scripts
347 | - Implement custom actions
348 | - Support script parameters
349 | - Create script management tools
350 | 
351 | **Implementation**:
352 | ```typescript
353 | // New tool: execute_server_script
354 | 
355 | export async function handleExecuteServerScript(request, erpnext, logger) {
356 |   const scriptName = request.params.arguments?.scriptName;
357 |   const parameters = request.params.arguments?.parameters || {};
358 |   
359 |   if (!scriptName) {
360 |     throw new McpError(ErrorCode.InvalidParams, "Script name is required");
361 |   }
362 |   
363 |   try {
364 |     // Execute server script
365 |     const result = await erpnext.executeServerScript(scriptName, parameters);
366 |     
367 |     return {
368 |       content: [{
369 |         type: "text",
370 |         text: JSON.stringify({
371 |           message: `Executed server script ${scriptName}`,
372 |           result
373 |         }, null, 2)
374 |       }]
375 |     };
376 |   } catch (error) {
377 |     return handleToolError(error, logger);
378 |   }
379 | }
380 | ```
381 | 
382 | ### 9. ERPNext Printing and PDF Generation
383 | 
384 | **Current Limitation**: No document formatting or printing.
385 | 
386 | **Proposed Improvements**:
387 | - Add print format rendering
388 | - Implement PDF generation
389 | - Support custom print templates
390 | - Create document export tools
391 | 
392 | **Implementation**:
393 | ```typescript
394 | // New tool: generate_pdf
395 | 
396 | export async function handleGeneratePdf(request, erpnext, logger) {
397 |   const doctype = request.params.arguments?.doctype;
398 |   const name = request.params.arguments?.name;
399 |   const printFormat = request.params.arguments?.printFormat;
400 |   const letterhead = request.params.arguments?.letterhead;
401 |   
402 |   if (!doctype || !name) {
403 |     throw new McpError(ErrorCode.InvalidParams, "DocType and document name are required");
404 |   }
405 |   
406 |   try {
407 |     // Generate PDF
408 |     const pdfData = await erpnext.generatePdf(doctype, name, printFormat, letterhead);
409 |     
410 |     return {
411 |       content: [{
412 |         type: "text",
413 |         text: JSON.stringify({
414 |           message: `Generated PDF for ${doctype} ${name}`,
415 |           pdf: pdfData // Base64 encoded
416 |         }, null, 2)
417 |       }]
418 |     };
419 |   } catch (error) {
420 |     return handleToolError(error, logger);
421 |   }
422 | }
423 | ```
424 | 
425 | ### 10. Interactive Tools
426 | 
427 | **Current Limitation**: All operations are direct API calls without user interaction.
428 | 
429 | **Proposed Improvements**:
430 | - Add support for multi-step interactive operations
431 | - Implement wizard-like flows
432 | - Support form generation
433 | - Create contextual suggestions
434 | 
435 | **Implementation**:
436 | ```typescript
437 | // New tool: start_interactive_flow
438 | 
439 | export async function handleStartInteractiveFlow(request, erpnext, logger) {
440 |   const flowType = request.params.arguments?.flowType;
441 |   const initialData = request.params.arguments?.initialData || {};
442 |   
443 |   if (!flowType) {
444 |     throw new McpError(ErrorCode.InvalidParams, "Flow type is required");
445 |   }
446 |   
447 |   try {
448 |     // Get flow definition
449 |     const flow = getFlowDefinition(flowType);
450 |     
451 |     // Initialize flow state
452 |     const flowState = initializeFlowState(flow, initialData);
453 |     
454 |     return {
455 |       content: [{
456 |         type: "text",
457 |         text: JSON.stringify({
458 |           message: `Started interactive flow: ${flowType}`,
459 |           currentStep: flowState.currentStep,
460 |           steps: flowState.steps,
461 |           totalSteps: flowState.totalSteps,
462 |           data: flowState.data,
463 |           form: generateFormForStep(flowState.currentStep, flow, flowState.data)
464 |         }, null, 2)
465 |       }]
466 |     };
467 |   } catch (error) {
468 |     return handleToolError(error, logger);
469 |   }
470 | }
471 | ```
472 | 
473 | ## Implementation Strategy
474 | 
475 | To implement these feature improvements, we recommend:
476 | 
477 | 1. **Prioritization**: Focus on features that provide the most value with the least implementation complexity first:
478 |    - Enhanced DocType discovery and metadata
479 |    - Advanced querying
480 |    - File operations
481 | 
482 | 2. **Phased Implementation**:
483 |    - Phase 1: Metadata and querying enhancements
484 |    - Phase 2: Bulk operations and file handling
485 |    - Phase 3: Workflows and synchronization
486 |    - Phase 4: Interactive tools and custom scripts
487 | 
488 | 3. **Dependency Handling**:
489 |    - Identify ERPNext API dependencies for each feature
490 |    - Document required permissions and configuration
491 |    - Handle version compatibility
492 | 
493 | 4. **Documentation and Examples**:
494 |    - Provide detailed documentation for each new feature
495 |    - Include usage examples
496 |    - Create tutorials for complex features
497 | 
498 | ## Conclusion
499 | 
500 | These feature improvements will significantly enhance the ERPNext MCP server's capabilities, making it a more powerful and flexible integration point for ERPNext. By implementing these features in a phased approach, we can incrementally add functionality while maintaining stability and backward compatibility.
501 | 
```

--------------------------------------------------------------------------------
/docs/error-handling-and-logging.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Error Handling and Logging Improvements
  2 | 
  3 | This document outlines recommendations for improving error handling and logging in the ERPNext MCP server.
  4 | 
  5 | ## Current Status
  6 | 
  7 | The current implementation has several limitations in its error handling and logging approach:
  8 | 
  9 | 1. Basic error handling with inconsistent patterns
 10 | 2. Console logging without structured format
 11 | 3. No distinct log levels for different environments
 12 | 4. No dedicated error mapping between ERPNext and MCP error codes
 13 | 5. Limited contextual information in error messages
 14 | 
 15 | ## Proposed Improvements
 16 | 
 17 | ### 1. Structured Error Handling
 18 | 
 19 | #### Create a dedicated Error Handling Module
 20 | 
 21 | ```typescript
 22 | // src/utils/error-handler.ts
 23 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
 24 | import { AxiosError } from "axios";
 25 | import { Logger } from "./logger";
 26 | 
 27 | export enum ERPNextErrorType {
 28 |   Authentication = "authentication",
 29 |   Permission = "permission",
 30 |   NotFound = "not_found",
 31 |   Validation = "validation",
 32 |   Server = "server",
 33 |   Network = "network",
 34 |   Unknown = "unknown"
 35 | }
 36 | 
 37 | export class ERPNextError extends Error {
 38 |   constructor(
 39 |     public readonly type: ERPNextErrorType,
 40 |     message: string,
 41 |     public readonly originalError?: Error
 42 |   ) {
 43 |     super(message);
 44 |     this.name = "ERPNextError";
 45 |   }
 46 | }
 47 | 
 48 | /**
 49 |  * Maps ERPNext errors to MCP error codes
 50 |  */
 51 | export function mapToMcpError(error: ERPNextError): McpError {
 52 |   switch (error.type) {
 53 |     case ERPNextErrorType.Authentication:
 54 |       return new McpError(ErrorCode.Unauthorized, error.message);
 55 |     case ERPNextErrorType.Permission:
 56 |       return new McpError(ErrorCode.Forbidden, error.message);
 57 |     case ERPNextErrorType.NotFound:
 58 |       return new McpError(ErrorCode.NotFound, error.message);
 59 |     case ERPNextErrorType.Validation:
 60 |       return new McpError(ErrorCode.InvalidParams, error.message);
 61 |     case ERPNextErrorType.Server:
 62 |       return new McpError(ErrorCode.InternalError, error.message);
 63 |     case ERPNextErrorType.Network:
 64 |       return new McpError(ErrorCode.InternalError, `Network error: ${error.message}`);
 65 |     default:
 66 |       return new McpError(ErrorCode.InternalError, `Unknown error: ${error.message}`);
 67 |   }
 68 | }
 69 | 
 70 | /**
 71 |  * Categorizes an error based on its properties and returns an ERPNextError
 72 |  */
 73 | export function categorizeError(error: any): ERPNextError {
 74 |   if (error instanceof ERPNextError) {
 75 |     return error;
 76 |   }
 77 |   
 78 |   // Handle Axios errors
 79 |   if (error?.isAxiosError) {
 80 |     const axiosError = error as AxiosError;
 81 |     const statusCode = axiosError.response?.status;
 82 |     const responseData = axiosError.response?.data as any;
 83 |     const message = responseData?.message || responseData?.error || axiosError.message;
 84 |     
 85 |     if (!statusCode) {
 86 |       return new ERPNextError(ERPNextErrorType.Network, 
 87 |         `Network error connecting to ERPNext: ${message}`, 
 88 |         error);
 89 |     }
 90 |     
 91 |     switch (statusCode) {
 92 |       case 401:
 93 |         return new ERPNextError(ERPNextErrorType.Authentication, 
 94 |           `Authentication failed: ${message}`, 
 95 |           error);
 96 |       case 403:
 97 |         return new ERPNextError(ERPNextErrorType.Permission, 
 98 |           `Permission denied: ${message}`, 
 99 |           error);
100 |       case 404:
101 |         return new ERPNextError(ERPNextErrorType.NotFound, 
102 |           `Resource not found: ${message}`, 
103 |           error);
104 |       case 400:
105 |         return new ERPNextError(ERPNextErrorType.Validation, 
106 |           `Validation error: ${message}`, 
107 |           error);
108 |       case 500:
109 |       case 502:
110 |       case 503:
111 |       case 504:
112 |         return new ERPNextError(ERPNextErrorType.Server, 
113 |           `ERPNext server error: ${message}`, 
114 |           error);
115 |       default:
116 |         return new ERPNextError(ERPNextErrorType.Unknown, 
117 |           `Unknown error: ${message}`, 
118 |           error);
119 |     }
120 |   }
121 |   
122 |   // Handle generic errors
123 |   return new ERPNextError(
124 |     ERPNextErrorType.Unknown, 
125 |     error?.message || 'An unknown error occurred',
126 |     error instanceof Error ? error : undefined
127 |   );
128 | }
129 | 
130 | /**
131 |  * Handles errors in tool handlers, returning appropriate response format
132 |  */
133 | export function handleToolError(error: any, logger: Logger) {
134 |   const erpError = categorizeError(error);
135 |   
136 |   // Log the error with appropriate context
137 |   if (erpError.originalError) {
138 |     logger.error(`${erpError.message}`, {
139 |       errorType: erpError.type,
140 |       originalError: erpError.originalError.message,
141 |       stack: erpError.originalError.stack
142 |     });
143 |   } else {
144 |     logger.error(`${erpError.message}`, { 
145 |       errorType: erpError.type,
146 |       stack: erpError.stack
147 |     });
148 |   }
149 |   
150 |   // Return a formatted error response
151 |   return {
152 |     content: [{
153 |       type: "text",
154 |       text: erpError.message
155 |     }],
156 |     isError: true
157 |   };
158 | }
159 | 
160 | /**
161 |  * Creates a correlation ID for tracking requests through the system
162 |  */
163 | export function createCorrelationId(): string {
164 |   return `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
165 | }
166 | ```
167 | 
168 | ### 2. Enhanced Logging System
169 | 
170 | #### Structured Logger Implementation
171 | 
172 | ```typescript
173 | // src/utils/logger.ts
174 | export enum LogLevel {
175 |   ERROR = 0,
176 |   WARN = 1,
177 |   INFO = 2,
178 |   DEBUG = 3
179 | }
180 | 
181 | export interface LogMetadata {
182 |   [key: string]: any;
183 | }
184 | 
185 | export interface LogEntry {
186 |   timestamp: string;
187 |   level: string;
188 |   message: string;
189 |   correlationId?: string;
190 |   metadata?: LogMetadata;
191 | }
192 | 
193 | export class Logger {
194 |   private level: LogLevel;
195 |   private serviceName: string;
196 |   
197 |   constructor(level: LogLevel = LogLevel.INFO, serviceName: string = "erpnext-mcp") {
198 |     this.level = level;
199 |     this.serviceName = serviceName;
200 |   }
201 |   
202 |   setLevel(level: LogLevel) {
203 |     this.level = level;
204 |   }
205 |   
206 |   private formatLog(level: string, message: string, metadata?: LogMetadata): LogEntry {
207 |     return {
208 |       timestamp: new Date().toISOString(),
209 |       level,
210 |       message,
211 |       correlationId: metadata?.correlationId,
212 |       metadata: metadata ? { ...metadata, service: this.serviceName } : { service: this.serviceName }
213 |     };
214 |   }
215 |   
216 |   private outputLog(logEntry: LogEntry) {
217 |     // In production, you might want to use a more sophisticated logging system
218 |     // For now, we'll use console with JSON formatting
219 |     const logJson = JSON.stringify(logEntry);
220 |     
221 |     switch (logEntry.level) {
222 |       case 'ERROR':
223 |         console.error(logJson);
224 |         break;
225 |       case 'WARN':
226 |         console.warn(logJson);
227 |         break;
228 |       case 'INFO':
229 |         console.info(logJson);
230 |         break;
231 |       case 'DEBUG':
232 |         console.debug(logJson);
233 |         break;
234 |       default:
235 |         console.log(logJson);
236 |     }
237 |   }
238 |   
239 |   error(message: string, metadata?: LogMetadata) {
240 |     if (this.level >= LogLevel.ERROR) {
241 |       this.outputLog(this.formatLog('ERROR', message, metadata));
242 |     }
243 |   }
244 |   
245 |   warn(message: string, metadata?: LogMetadata) {
246 |     if (this.level >= LogLevel.WARN) {
247 |       this.outputLog(this.formatLog('WARN', message, metadata));
248 |     }
249 |   }
250 |   
251 |   info(message: string, metadata?: LogMetadata) {
252 |     if (this.level >= LogLevel.INFO) {
253 |       this.outputLog(this.formatLog('INFO', message, metadata));
254 |     }
255 |   }
256 |   
257 |   debug(message: string, metadata?: LogMetadata) {
258 |     if (this.level >= LogLevel.DEBUG) {
259 |       this.outputLog(this.formatLog('DEBUG', message, metadata));
260 |     }
261 |   }
262 | }
263 | ```
264 | 
265 | ### 3. Request Context and Correlation
266 | 
267 | To track requests through the system:
268 | 
269 | ```typescript
270 | // src/middleware/context.ts
271 | import { createCorrelationId } from "../utils/error-handler";
272 | 
273 | export interface RequestContext {
274 |   correlationId: string;
275 |   startTime: number;
276 |   [key: string]: any;
277 | }
278 | 
279 | export class RequestContextManager {
280 |   private static contextMap = new Map<string, RequestContext>();
281 |   
282 |   static createContext(requestId: string): RequestContext {
283 |     const context: RequestContext = {
284 |       correlationId: createCorrelationId(),
285 |       startTime: Date.now()
286 |     };
287 |     
288 |     this.contextMap.set(requestId, context);
289 |     return context;
290 |   }
291 |   
292 |   static getContext(requestId: string): RequestContext | undefined {
293 |     return this.contextMap.get(requestId);
294 |   }
295 |   
296 |   static removeContext(requestId: string): void {
297 |     this.contextMap.delete(requestId);
298 |   }
299 | }
300 | ```
301 | 
302 | ### 4. Integration with MCP Server
303 | 
304 | Use the error handling and logging systems with the MCP server:
305 | 
306 | ```typescript
307 | // src/index.ts
308 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
309 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
310 | import { Config } from "./utils/config";
311 | import { Logger, LogLevel } from "./utils/logger";
312 | import { RequestContextManager } from "./middleware/context";
313 | 
314 | async function main() {
315 |   // Initialize logger
316 |   const logger = new Logger(
317 |     process.env.DEBUG ? LogLevel.DEBUG : LogLevel.INFO
318 |   );
319 |   
320 |   try {
321 |     logger.info("Initializing ERPNext MCP server");
322 |     
323 |     // Create server
324 |     const server = new Server(
325 |       {
326 |         name: "erpnext-server",
327 |         version: "0.1.0"
328 |       },
329 |       {
330 |         capabilities: {
331 |           resources: {},
332 |           tools: {}
333 |         }
334 |       }
335 |     );
336 |     
337 |     // Set up request interceptors for correlation and context
338 |     server.onRequest = (request) => {
339 |       const context = RequestContextManager.createContext(request.id);
340 |       logger.debug(`Received request: ${request.method}`, {
341 |         correlationId: context.correlationId,
342 |         request: {
343 |           id: request.id,
344 |           method: request.method,
345 |           params: JSON.stringify(request.params)
346 |         }
347 |       });
348 |     };
349 |     
350 |     // Set up response interceptors for timing and cleanup
351 |     server.onResponse = (response) => {
352 |       const context = RequestContextManager.getContext(response.id);
353 |       if (context) {
354 |         const duration = Date.now() - context.startTime;
355 |         logger.debug(`Sending response`, {
356 |           correlationId: context.correlationId,
357 |           response: {
358 |             id: response.id,
359 |             duration: `${duration}ms`,
360 |             hasError: !!response.error
361 |           }
362 |         });
363 |         
364 |         // Clean up context
365 |         RequestContextManager.removeContext(response.id);
366 |       }
367 |     };
368 |     
369 |     // Set up error handler for server errors
370 |     server.onerror = (error) => {
371 |       logger.error("Server error", { error: error?.message, stack: error?.stack });
372 |     };
373 |     
374 |     // Connect server with stdio transport
375 |     const transport = new StdioServerTransport();
376 |     await server.connect(transport);
377 |     logger.info('ERPNext MCP server running on stdio');
378 |     
379 |   } catch (error) {
380 |     logger.error("Failed to start server", { error: error?.message, stack: error?.stack });
381 |     process.exit(1);
382 |   }
383 | }
384 | 
385 | main();
386 | ```
387 | 
388 | ### 5. Integrating with Client
389 | 
390 | Update the ERPNext client to use the enhanced error handling:
391 | 
392 | ```typescript
393 | // src/client/erpnext-client.ts
394 | import axios, { AxiosInstance } from "axios";
395 | import { Logger } from "../utils/logger";
396 | import { Config } from "../utils/config";
397 | import { categorizeError, ERPNextError, ERPNextErrorType } from "../utils/error-handler";
398 | 
399 | export class ERPNextClient {
400 |   private baseUrl: string;
401 |   private axiosInstance: AxiosInstance;
402 |   private authenticated: boolean = false;
403 |   private logger: Logger;
404 | 
405 |   constructor(config: Config, logger: Logger) {
406 |     this.logger = logger;
407 |     this.baseUrl = config.getERPNextUrl();
408 |     
409 |     // Initialize axios instance
410 |     this.axiosInstance = axios.create({
411 |       baseURL: this.baseUrl,
412 |       withCredentials: true,
413 |       headers: {
414 |         'Content-Type': 'application/json',
415 |         'Accept': 'application/json'
416 |       }
417 |     });
418 |     
419 |     // Add request interceptor for logging
420 |     this.axiosInstance.interceptors.request.use(
421 |       (config) => {
422 |         this.logger.debug(`Sending ${config.method?.toUpperCase()} request to ${config.url}`, {
423 |           api: {
424 |             method: config.method,
425 |             url: config.url,
426 |             hasData: !!config.data,
427 |             hasParams: !!config.params
428 |           }
429 |         });
430 |         return config;
431 |       },
432 |       (error) => {
433 |         this.logger.error(`Request error: ${error.message}`);
434 |         return Promise.reject(error);
435 |       }
436 |     );
437 |     
438 |     // Add response interceptor for error handling
439 |     this.axiosInstance.interceptors.response.use(
440 |       (response) => {
441 |         return response;
442 |       },
443 |       (error) => {
444 |         const erpError = categorizeError(error);
445 |         this.logger.error(`API error: ${erpError.message}`, {
446 |           errorType: erpError.type,
447 |           statusCode: error.response?.status,
448 |           data: error.response?.data
449 |         });
450 |         return Promise.reject(erpError);
451 |       }
452 |     );
453 |     
454 |     // Configure authentication if credentials provided
455 |     const apiKey = config.getERPNextApiKey();
456 |     const apiSecret = config.getERPNextApiSecret();
457 |     
458 |     if (apiKey && apiSecret) {
459 |       this.axiosInstance.defaults.headers.common['Authorization'] = 
460 |         `token ${apiKey}:${apiSecret}`;
461 |       this.authenticated = true;
462 |       this.logger.info("Initialized with API key authentication");
463 |     }
464 |   }
465 | 
466 |   // Client methods would use the same error handling pattern...
467 |   async login(username: string, password: string): Promise<void> {
468 |     try {
469 |       this.logger.info(`Attempting login for user ${username}`);
470 |       const response = await this.axiosInstance.post('/api/method/login', {
471 |         usr: username,
472 |         pwd: password
473 |       });
474 |       
475 |       if (response.data.message === 'Logged In') {
476 |         this.authenticated = true;
477 |         this.logger.info(`Successfully authenticated user ${username}`);
478 |       } else {
479 |         throw new ERPNextError(
480 |           ERPNextErrorType.Authentication,
481 |           "Login response did not confirm successful authentication"
482 |         );
483 |       }
484 |     } catch (error) {
485 |       // Let the interceptor handle the error categorization
486 |       this.authenticated = false;
487 |       throw error;
488 |     }
489 |   }
490 | 
491 |   // Other methods would follow the same pattern...
492 | }
493 | ```
494 | 
495 | ## Benefits of Improved Error Handling and Logging
496 | 
497 | 1. **Better Diagnostics**: Structured logging with correlation IDs makes it easier to track requests through the system and diagnose issues.
498 | 
499 | 2. **Consistent Error Responses**: Standardized error handling ensures that clients receive consistent and informative error messages.
500 | 
501 | 3. **Categorized Errors**: Errors are properly categorized, making it easier to handle different types of failures appropriately.
502 | 
503 | 4. **Contextual Information**: Additional metadata is included with logs, providing more context for troubleshooting.
504 | 
505 | 5. **Performance Tracking**: Request timing information helps identify performance bottlenecks.
506 | 
507 | 6. **Environment-specific Logging**: Different log levels can be used in development vs. production environments.
508 | 
509 | ## Implementation Plan
510 | 
511 | 1. Create the error handling utility module
512 | 2. Implement the structured logger
513 | 3. Update the client to use the enhanced error handling
514 | 4. Update resource and tool handlers to leverage the new error system
515 | 5. Add request context management
516 | 6. Update the main server to integrate all components
517 | 
518 | These changes will significantly improve the reliability and maintainability of the ERPNext MCP server by making errors more traceable and logs more informative.
519 | 
```

--------------------------------------------------------------------------------
/docs/testing-strategy.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Testing Strategy
  2 | 
  3 | This document outlines a comprehensive testing strategy for the ERPNext MCP server to ensure reliability, correctness, and maintainability.
  4 | 
  5 | ## Current Status
  6 | 
  7 | The current implementation lacks any automated testing, which poses several risks:
  8 | 
  9 | - No way to verify that changes don't break existing functionality
 10 | - Difficulty in detecting regressions
 11 | - Challenges in maintaining code quality as the codebase grows
 12 | - Decreased confidence when making changes or adding features
 13 | 
 14 | ## Proposed Testing Structure
 15 | 
 16 | ### Directory Structure
 17 | 
 18 | ```
 19 | tests/
 20 | ├── unit/
 21 | │   ├── client/
 22 | │   │   └── erpnext-client.test.ts
 23 | │   ├── handlers/
 24 | │   │   ├── resource-handlers.test.ts
 25 | │   │   └── tool-handlers.test.ts
 26 | │   └── utils/
 27 | │       ├── cache.test.ts
 28 | │       ├── config.test.ts
 29 | │       └── logger.test.ts
 30 | ├── integration/
 31 | │   └── api-integration.test.ts
 32 | ├── e2e/
 33 | │   └── full-workflow.test.ts
 34 | └── mocks/
 35 |     ├── erpnext-api.ts
 36 |     └── test-data.ts
 37 | ```
 38 | 
 39 | ## Testing Levels
 40 | 
 41 | ### 1. Unit Tests
 42 | 
 43 | Unit tests should focus on testing individual components in isolation, mocking external dependencies. These tests should be fast and focused.
 44 | 
 45 | #### Example: Testing the Cache Utility
 46 | 
 47 | ```typescript
 48 | // tests/unit/utils/cache.test.ts
 49 | import { Cache } from '../../../src/utils/cache';
 50 | import { jest } from '@jest/globals';
 51 | 
 52 | describe('Cache', () => {
 53 |   let cache: Cache;
 54 |   
 55 |   beforeEach(() => {
 56 |     cache = new Cache();
 57 |     jest.useFakeTimers();
 58 |   });
 59 |   
 60 |   afterEach(() => {
 61 |     jest.useRealTimers();
 62 |   });
 63 |   
 64 |   test('should store and retrieve values', () => {
 65 |     // Arrange
 66 |     const key = 'test-key';
 67 |     const value = { data: 'test-data' };
 68 |     
 69 |     // Act
 70 |     cache.set(key, value);
 71 |     const result = cache.get(key);
 72 |     
 73 |     // Assert
 74 |     expect(result).toEqual(value);
 75 |   });
 76 |   
 77 |   test('should expire values after TTL', () => {
 78 |     // Arrange
 79 |     const key = 'test-key';
 80 |     const value = { data: 'test-data' };
 81 |     const ttl = 1000; // 1 second
 82 |     
 83 |     // Act
 84 |     cache.set(key, value, ttl);
 85 |     
 86 |     // Move time forward
 87 |     jest.advanceTimersByTime(1001);
 88 |     
 89 |     const result = cache.get(key);
 90 |     
 91 |     // Assert
 92 |     expect(result).toBeUndefined();
 93 |   });
 94 |   
 95 |   test('should invalidate keys with prefix', () => {
 96 |     // Arrange
 97 |     cache.set('prefix:key1', 'value1');
 98 |     cache.set('prefix:key2', 'value2');
 99 |     cache.set('other:key', 'value3');
100 |     
101 |     // Act
102 |     cache.invalidate('prefix:');
103 |     
104 |     // Assert
105 |     expect(cache.get('prefix:key1')).toBeUndefined();
106 |     expect(cache.get('prefix:key2')).toBeUndefined();
107 |     expect(cache.get('other:key')).toBe('value3');
108 |   });
109 | });
110 | ```
111 | 
112 | #### Example: Testing the ERPNext Client
113 | 
114 | ```typescript
115 | // tests/unit/client/erpnext-client.test.ts
116 | import { ERPNextClient } from '../../../src/client/erpnext-client';
117 | import { Config } from '../../../src/utils/config';
118 | import { Logger } from '../../../src/utils/logger';
119 | import axios from 'axios';
120 | import { jest } from '@jest/globals';
121 | 
122 | // Mock axios
123 | jest.mock('axios');
124 | const mockedAxios = axios as jest.Mocked<typeof axios>;
125 | 
126 | describe('ERPNextClient', () => {
127 |   let client: ERPNextClient;
128 |   let mockConfig: jest.Mocked<Config>;
129 |   let mockLogger: jest.Mocked<Logger>;
130 |   
131 |   beforeEach(() => {
132 |     // Setup mock config
133 |     mockConfig = {
134 |       getERPNextUrl: jest.fn().mockReturnValue('https://test-erpnext.com'),
135 |       getERPNextApiKey: jest.fn().mockReturnValue('test-key'),
136 |       getERPNextApiSecret: jest.fn().mockReturnValue('test-secret')
137 |     } as unknown as jest.Mocked<Config>;
138 |     
139 |     // Setup mock logger
140 |     mockLogger = {
141 |       info: jest.fn(),
142 |       error: jest.fn(),
143 |       debug: jest.fn(),
144 |       warn: jest.fn()
145 |     } as unknown as jest.Mocked<Logger>;
146 |     
147 |     // Setup axios mock
148 |     mockedAxios.create.mockReturnValue({
149 |       defaults: { headers: { common: {} } },
150 |       get: jest.fn(),
151 |       post: jest.fn(),
152 |       put: jest.fn()
153 |     } as any);
154 |     
155 |     // Create client instance
156 |     client = new ERPNextClient(mockConfig, mockLogger);
157 |   });
158 |   
159 |   test('should initialize with API key authentication', () => {
160 |     expect(mockLogger.info).toHaveBeenCalledWith(
161 |       expect.stringContaining('API key authentication')
162 |     );
163 |     expect(client.isAuthenticated()).toBe(true);
164 |   });
165 |   
166 |   test('login should authenticate the client', async () => {
167 |     // Setup mock response
168 |     const axiosInstance = mockedAxios.create.mock.results[0].value;
169 |     axiosInstance.post.mockResolvedValue({
170 |       data: { message: 'Logged In' }
171 |     });
172 |     
173 |     // Act
174 |     await client.login('testuser', 'testpass');
175 |     
176 |     // Assert
177 |     expect(axiosInstance.post).toHaveBeenCalledWith(
178 |       '/api/method/login',
179 |       { usr: 'testuser', pwd: 'testpass' }
180 |     );
181 |     expect(client.isAuthenticated()).toBe(true);
182 |   });
183 |   
184 |   test('login should handle authentication failure', async () => {
185 |     // Setup mock response for failure
186 |     const axiosInstance = mockedAxios.create.mock.results[0].value;
187 |     axiosInstance.post.mockRejectedValue(new Error('Authentication failed'));
188 |     
189 |     // Act & Assert
190 |     await expect(client.login('testuser', 'wrongpass')).rejects.toThrow('Authentication failed');
191 |     expect(client.isAuthenticated()).toBe(false);
192 |   });
193 | });
194 | ```
195 | 
196 | ### 2. Integration Tests
197 | 
198 | Integration tests should verify that different components work correctly together. For the ERPNext MCP server, integration tests should focus on the interaction between the server, handlers, and the ERPNext client.
199 | 
200 | ```typescript
201 | // tests/integration/api-integration.test.ts
202 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
203 | import { MemoryTransport } from "@modelcontextprotocol/sdk/server/memory.js";
204 | import { ERPNextClient } from '../../src/client/erpnext-client';
205 | import { registerResourceHandlers } from '../../src/handlers/resource-handlers';
206 | import { registerToolHandlers } from '../../src/handlers/tool-handlers';
207 | import { Logger } from '../../src/utils/logger';
208 | import { Cache } from '../../src/utils/cache';
209 | import { jest } from '@jest/globals';
210 | 
211 | // Use mock ERPNext client
212 | jest.mock('../../src/client/erpnext-client');
213 | 
214 | describe('MCP Server Integration', () => {
215 |   let server: Server;
216 |   let transport: MemoryTransport;
217 |   let mockErpnextClient: jest.Mocked<ERPNextClient>;
218 |   let logger: Logger;
219 |   let cache: Cache;
220 |   
221 |   beforeEach(async () => {
222 |     // Setup mocks
223 |     mockErpnextClient = {
224 |       isAuthenticated: jest.fn().mockReturnValue(true),
225 |       login: jest.fn(),
226 |       getDocument: jest.fn(),
227 |       getDocList: jest.fn(),
228 |       createDocument: jest.fn(),
229 |       updateDocument: jest.fn(),
230 |       runReport: jest.fn(),
231 |       getAllDocTypes: jest.fn()
232 |     } as unknown as jest.Mocked<ERPNextClient>;
233 |     
234 |     logger = new Logger();
235 |     cache = new Cache();
236 |     
237 |     // Create server
238 |     server = new Server(
239 |       {
240 |         name: "erpnext-server-test",
241 |         version: "0.1.0"
242 |       },
243 |       {
244 |         capabilities: {
245 |           resources: {},
246 |           tools: {}
247 |         }
248 |       }
249 |     );
250 |     
251 |     // Register handlers
252 |     registerResourceHandlers(server, mockErpnextClient, cache, logger);
253 |     registerToolHandlers(server, mockErpnextClient, logger);
254 |     
255 |     // Setup transport
256 |     transport = new MemoryTransport();
257 |     await server.connect(transport);
258 |   });
259 |   
260 |   afterEach(async () => {
261 |     await server.close();
262 |   });
263 |   
264 |   test('List tools should return all tools', async () => {
265 |     // Act
266 |     const response = await transport.sendRequestAndWaitForResponse({
267 |       jsonrpc: "2.0",
268 |       id: "test-id",
269 |       method: "mcp.listTools",
270 |       params: {}
271 |     });
272 |     
273 |     // Assert
274 |     expect(response.result).toBeDefined();
275 |     expect(response.result.tools).toBeInstanceOf(Array);
276 |     expect(response.result.tools.length).toBeGreaterThan(0);
277 |     expect(response.result.tools.map(t => t.name)).toContain('authenticate_erpnext');
278 |   });
279 |   
280 |   test('Get documents tool should fetch documents', async () => {
281 |     // Arrange
282 |     const mockDocuments = [
283 |       { name: "CUST-001", customer_name: "Test Customer" }
284 |     ];
285 |     
286 |     mockErpnextClient.getDocList.mockResolvedValue(mockDocuments);
287 |     
288 |     // Act
289 |     const response = await transport.sendRequestAndWaitForResponse({
290 |       jsonrpc: "2.0",
291 |       id: "test-id",
292 |       method: "mcp.callTool",
293 |       params: {
294 |         name: "get_documents",
295 |         arguments: {
296 |           doctype: "Customer"
297 |         }
298 |       }
299 |     });
300 |     
301 |     // Assert
302 |     expect(mockErpnextClient.getDocList).toHaveBeenCalledWith(
303 |       "Customer", undefined, undefined, undefined
304 |     );
305 |     expect(response.result).toBeDefined();
306 |     expect(JSON.parse(response.result.content[0].text)).toEqual(mockDocuments);
307 |   });
308 | });
309 | ```
310 | 
311 | ### 3. End-to-End Tests
312 | 
313 | E2E tests should verify the entire system works correctly from the user's perspective. These tests should use a real or mock ERPNext server and execute complete workflows.
314 | 
315 | ```typescript
316 | // tests/e2e/full-workflow.test.ts
317 | import { spawn, ChildProcess } from 'child_process';
318 | import axios from 'axios';
319 | import { jest } from '@jest/globals';
320 | import path from 'path';
321 | import { startMockErpnextServer, stopMockErpnextServer } from '../mocks/erpnext-api';
322 | 
323 | describe('E2E Tests', () => {
324 |   let serverProcess: ChildProcess;
325 |   let mockServerUrl: string;
326 |   
327 |   beforeAll(async () => {
328 |     // Start mock ERPNext server
329 |     mockServerUrl = await startMockErpnextServer();
330 |     
331 |     // Start MCP server with environment pointing to mock server
332 |     const serverPath = path.resolve(__dirname, '../../build/index.js');
333 |     serverProcess = spawn('node', [serverPath], {
334 |       env: {
335 |         ...process.env,
336 |         ERPNEXT_URL: mockServerUrl,
337 |         ERPNEXT_API_KEY: 'test-key',
338 |         ERPNEXT_API_SECRET: 'test-secret',
339 |         DEBUG: 'true'
340 |       },
341 |       stdio: ['pipe', 'pipe', 'pipe']
342 |     });
343 |     
344 |     // Wait for server to start
345 |     await new Promise(resolve => setTimeout(resolve, 1000));
346 |   });
347 |   
348 |   afterAll(async () => {
349 |     // Terminate the server process
350 |     if (serverProcess) {
351 |       serverProcess.kill();
352 |     }
353 |     
354 |     // Stop mock server
355 |     await stopMockErpnextServer();
356 |   });
357 |   
358 |   test('Complete workflow test', async () => {
359 |     // Setup a test client that communicates with the MCP server
360 |     
361 |     // This would test a complete workflow:
362 |     // 1. Authentication
363 |     // 2. Document retrieval
364 |     // 3. Document creation
365 |     // 4. Document update
366 |     // 5. Running a report
367 |   });
368 | });
369 | ```
370 | 
371 | ## Test Mocks
372 | 
373 | Creating proper mocks is essential for effective testing:
374 | 
375 | ```typescript
376 | // tests/mocks/erpnext-api.ts
377 | import express from 'express';
378 | import http from 'http';
379 | import bodyParser from 'body-parser';
380 | 
381 | let server: http.Server;
382 | let app: express.Express;
383 | 
384 | // Mock data storage
385 | const mockData: Record<string, any[]> = {
386 |   'Customer': [
387 |     {
388 |       name: 'CUST-001',
389 |       customer_name: 'Test Customer 1',
390 |       customer_type: 'Company',
391 |       customer_group: 'Commercial',
392 |       territory: 'United States'
393 |     }
394 |   ],
395 |   'Item': [
396 |     {
397 |       name: 'ITEM-001',
398 |       item_code: 'ITEM-001',
399 |       item_name: 'Test Item 1',
400 |       item_group: 'Products',
401 |       stock_uom: 'Nos'
402 |     }
403 |   ]
404 | };
405 | 
406 | export async function startMockErpnextServer(): Promise<string> {
407 |   app = express();
408 |   app.use(bodyParser.json());
409 |   
410 |   // Setup API endpoints that mimic ERPNext
411 |   
412 |   // Login endpoint
413 |   app.post('/api/method/login', (req, res) => {
414 |     const { usr, pwd } = req.body;
415 |     
416 |     if (usr === 'testuser' && pwd === 'testpass') {
417 |       res.json({ message: 'Logged In' });
418 |     } else {
419 |       res.status(401).json({ message: 'Authentication failed' });
420 |     }
421 |   });
422 |   
423 |   // Document listing
424 |   app.get('/api/resource/:doctype', (req, res) => {
425 |     const doctype = req.params.doctype;
426 |     res.json({ data: mockData[doctype] || [] });
427 |   });
428 |   
429 |   // Document retrieval
430 |   app.get('/api/resource/:doctype/:name', (req, res) => {
431 |     const { doctype, name } = req.params;
432 |     const docs = mockData[doctype] || [];
433 |     const doc = docs.find(d => d.name === name);
434 |     
435 |     if (doc) {
436 |       res.json({ data: doc });
437 |     } else {
438 |       res.status(404).json({ message: 'Not found' });
439 |     }
440 |   });
441 |   
442 |   // Document creation
443 |   app.post('/api/resource/:doctype', (req, res) => {
444 |     const { doctype } = req.params;
445 |     const data = req.body.data;
446 |     
447 |     if (!mockData[doctype]) {
448 |       mockData[doctype] = [];
449 |     }
450 |     
451 |     mockData[doctype].push(data);
452 |     res.json({ data });
453 |   });
454 |   
455 |   // Document update
456 |   app.put('/api/resource/:doctype/:name', (req, res) => {
457 |     const { doctype, name } = req.params;
458 |     const data = req.body.data;
459 |     
460 |     if (!mockData[doctype]) {
461 |       return res.status(404).json({ message: 'DocType not found' });
462 |     }
463 |     
464 |     const index = mockData[doctype].findIndex(d => d.name === name);
465 |     if (index === -1) {
466 |       return res.status(404).json({ message: 'Document not found' });
467 |     }
468 |     
469 |     mockData[doctype][index] = { ...mockData[doctype][index], ...data };
470 |     res.json({ data: mockData[doctype][index] });
471 |   });
472 |   
473 |   // Start server on a random port
474 |   return new Promise<string>((resolve) => {
475 |     server = app.listen(0, () => {
476 |       const address = server.address();
477 |       const port = typeof address === 'object' ? address?.port : 0;
478 |       resolve(`http://localhost:${port}`);
479 |     });
480 |   });
481 | }
482 | 
483 | export async function stopMockErpnextServer(): Promise<void> {
484 |   return new Promise<void>((resolve) => {
485 |     if (server) {
486 |       server.close(() => resolve());
487 |     } else {
488 |       resolve();
489 |     }
490 |   });
491 | }
492 | ```
493 | 
494 | ## Setting Up Testing Infrastructure
495 | 
496 | ### 1. Dependencies
497 | 
498 | Add the following development dependencies to `package.json`:
499 | 
500 | ```json
501 | "devDependencies": {
502 |   "@types/jest": "^29.5.0",
503 |   "@types/express": "^4.17.17",
504 |   "body-parser": "^1.20.2",
505 |   "express": "^4.18.2",
506 |   "jest": "^29.5.0",
507 |   "ts-jest": "^29.1.0",
508 |   "@types/node": "^20.11.24",
509 |   "typescript": "^5.3.3"
510 | }
511 | ```
512 | 
513 | ### 2. Jest Configuration
514 | 
515 | Add Jest configuration in `package.json`:
516 | 
517 | ```json
518 | "jest": {
519 |   "preset": "ts-jest",
520 |   "testEnvironment": "node",
521 |   "roots": [
522 |     "<rootDir>/tests"
523 |   ],
524 |   "collectCoverage": true,
525 |   "collectCoverageFrom": [
526 |     "src/**/*.ts"
527 |   ],
528 |   "coverageThreshold": {
529 |     "global": {
530 |       "branches": 80,
531 |       "functions": 80,
532 |       "lines": 80,
533 |       "statements": 80
534 |     }
535 |   }
536 | }
537 | ```
538 | 
539 | ### 3. Add Test Scripts
540 | 
541 | Add test scripts to `package.json`:
542 | 
543 | ```json
544 | "scripts": {
545 |   "test": "jest",
546 |   "test:watch": "jest --watch",
547 |   "test:coverage": "jest --coverage",
548 |   "test:unit": "jest tests/unit",
549 |   "test:integration": "jest tests/integration",
550 |   "test:e2e": "jest tests/e2e"
551 | }
552 | ```
553 | 
554 | ## CI/CD Integration
555 | 
556 | Implement continuous integration using GitHub Actions or similar CI platform:
557 | 
558 | ```yaml
559 | # .github/workflows/ci.yml
560 | name: CI
561 | 
562 | on:
563 |   push:
564 |     branches: [ main ]
565 |   pull_request:
566 |     branches: [ main ]
567 | 
568 | jobs:
569 |   test:
570 |     runs-on: ubuntu-latest
571 | 
572 |     steps:
573 |     - uses: actions/checkout@v3
574 |     
575 |     - name: Setup Node.js
576 |       uses: actions/setup-node@v3
577 |       with:
578 |         node-version: '18'
579 |         
580 |     - name: Install dependencies
581 |       run: npm ci
582 |       
583 |     - name: Run tests
584 |       run: npm test
585 |       
586 |     - name: Upload coverage
587 |       uses: codecov/codecov-action@v3
588 | ```
589 | 
590 | ## Testing Best Practices
591 | 
592 | 1. **Write tests before code** - Consider test-driven development (TDD) for new features
593 | 2. **Test edge cases** - Ensure error scenarios and unusual inputs are handled correctly
594 | 3. **Keep tests independent** - Each test should run in isolation
595 | 4. **Use descriptive test names** - Tests should document what functionality is being verified
596 | 5. **Mock external dependencies** - Don't rely on external services in unit tests
597 | 6. **Aim for high coverage** - But focus on meaningful coverage rather than arbitrary metrics
598 | 7. **Maintain tests** - Update tests when functionality changes
599 | 8. **Run tests regularly** - Integrate in CI/CD pipeline and run locally before commits
600 | 
601 | ## Conclusion
602 | 
603 | Implementing a comprehensive testing strategy will significantly improve the reliability and maintainability of the ERPNext MCP server. By using a combination of unit, integration, and end-to-end tests, we can ensure that the server behaves correctly under different scenarios and that changes don't introduce regressions.
604 | 
```

--------------------------------------------------------------------------------
/docs/security-and-authentication.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Security and Authentication Improvements
  2 | 
  3 | This document outlines recommendations for enhancing security and authentication in the ERPNext MCP server.
  4 | 
  5 | ## Current Status
  6 | 
  7 | The current implementation has several limitations in its security and authentication approach:
  8 | 
  9 | 1. Simple username/password authentication without token refresh mechanisms
 10 | 2. Basic API key/secret handling without proper validation
 11 | 3. Passwords transmitted and handled in plain text
 12 | 4. No input validation or sanitization
 13 | 5. No token/session management
 14 | 6. Limited security headers
 15 | 7. No protection against common security vulnerabilities
 16 | 
 17 | ## Proposed Improvements
 18 | 
 19 | ### 1. Enhanced Authentication System
 20 | 
 21 | #### Robust Authentication Mechanisms
 22 | 
 23 | ```typescript
 24 | // src/auth/authenticator.ts
 25 | import { Logger } from "../utils/logger";
 26 | import { Config } from "../utils/config";
 27 | import { ERPNextClient } from "../client/erpnext-client";
 28 | import { ERPNextError, ERPNextErrorType } from "../utils/error-handler";
 29 | 
 30 | export interface AuthTokens {
 31 |   accessToken: string;
 32 |   refreshToken?: string;
 33 |   expiresAt?: number;
 34 | }
 35 | 
 36 | export class AuthManager {
 37 |   private tokens: AuthTokens | null = null;
 38 |   private authenticated: boolean = false;
 39 |   private refreshTimer: NodeJS.Timeout | null = null;
 40 |   private client: ERPNextClient;
 41 |   private logger: Logger;
 42 |   
 43 |   constructor(
 44 |     client: ERPNextClient, 
 45 |     logger: Logger,
 46 |     private readonly config: Config
 47 |   ) {
 48 |     this.client = client;
 49 |     this.logger = logger;
 50 |     
 51 |     // Initialize with API key/secret if available
 52 |     const apiKey = this.config.getERPNextApiKey();
 53 |     const apiSecret = this.config.getERPNextApiSecret();
 54 |     
 55 |     if (apiKey && apiSecret) {
 56 |       this.authenticated = true;
 57 |       this.logger.info("Initialized with API key authentication");
 58 |     }
 59 |   }
 60 |   
 61 |   /**
 62 |    * Check if the client is authenticated
 63 |    */
 64 |   isAuthenticated(): boolean {
 65 |     return this.authenticated;
 66 |   }
 67 |   
 68 |   /**
 69 |    * Authenticate using username and password
 70 |    */
 71 |   async authenticate(username: string, password: string): Promise<void> {
 72 |     try {
 73 |       if (!username || !password) {
 74 |         throw new ERPNextError(
 75 |           ERPNextErrorType.Authentication, 
 76 |           "Username and password are required"
 77 |         );
 78 |       }
 79 |       
 80 |       // Mask password in logs
 81 |       this.logger.info(`Attempting to authenticate user: ${username}`);
 82 |       
 83 |       // Authenticate with ERPNext
 84 |       await this.client.login(username, password);
 85 |       
 86 |       this.authenticated = true;
 87 |       this.logger.info(`Authentication successful for user: ${username}`);
 88 |       
 89 |       // In a real implementation, we would store tokens and set up refresh
 90 |       // ERPNext doesn't have a standard token-based auth, but we're 
 91 |       // setting up the structure for future enhancement
 92 |     } catch (error) {
 93 |       this.authenticated = false;
 94 |       this.logger.error(`Authentication failed for user: ${username}`);
 95 |       throw error;
 96 |     }
 97 |   }
 98 |   
 99 |   /**
100 |    * Set up token refresh mechanism
101 |    * This is a placeholder for future enhancement as ERPNext's
102 |    * standard API doesn't use refresh tokens, but the structure
103 |    * is here for custom implementations or future changes
104 |    */
105 |   private setupTokenRefresh(tokens: AuthTokens): void {
106 |     // Clear any existing refresh timer
107 |     if (this.refreshTimer) {
108 |       clearTimeout(this.refreshTimer);
109 |     }
110 |     
111 |     // If no expiry, don't set up refresh
112 |     if (!tokens.expiresAt || !tokens.refreshToken) {
113 |       return;
114 |     }
115 |     
116 |     // Calculate time until refresh (5 minutes before expiry)
117 |     const now = Date.now();
118 |     const expiryTime = tokens.expiresAt;
119 |     const timeUntilRefresh = Math.max(0, expiryTime - now - 5 * 60 * 1000);
120 |     
121 |     this.logger.debug(`Setting up token refresh in ${timeUntilRefresh / 1000} seconds`);
122 |     
123 |     this.refreshTimer = setTimeout(async () => {
124 |       try {
125 |         // Here we would implement the token refresh logic
126 |         this.logger.debug("Refreshing authentication token");
127 |         
128 |         // For future implementation:
129 |         // const newTokens = await this.client.refreshToken(tokens.refreshToken);
130 |         // this.setTokens(newTokens);
131 |       } catch (error) {
132 |         this.logger.error("Failed to refresh token", { error });
133 |         this.authenticated = false;
134 |       }
135 |     }, timeUntilRefresh);
136 |   }
137 |   
138 |   /**
139 |    * Store authentication tokens
140 |    */
141 |   private setTokens(tokens: AuthTokens): void {
142 |     this.tokens = tokens;
143 |     this.authenticated = true;
144 |     
145 |     // Set up token refresh if applicable
146 |     this.setupTokenRefresh(tokens);
147 |   }
148 |   
149 |   /**
150 |    * Get current auth token for API requests
151 |    */
152 |   getAuthToken(): string | null {
153 |     return this.tokens?.accessToken || null;
154 |   }
155 |   
156 |   /**
157 |    * Log out and clear authentication
158 |    */
159 |   logout(): void {
160 |     this.authenticated = false;
161 |     this.tokens = null;
162 |     
163 |     if (this.refreshTimer) {
164 |       clearTimeout(this.refreshTimer);
165 |       this.refreshTimer = null;
166 |     }
167 |     
168 |     this.logger.info("User logged out");
169 |   }
170 | }
171 | ```
172 | 
173 | ### 2. Input Validation and Sanitization
174 | 
175 | Create a dedicated validation module to ensure inputs are properly validated before being used:
176 | 
177 | ```typescript
178 | // src/utils/validation.ts
179 | import { z } from "zod"; // Using Zod for schema validation
180 | 
181 | // Schema for authentication credentials
182 | export const AuthCredentialsSchema = z.object({
183 |   username: z.string().min(1, "Username is required"),
184 |   password: z.string().min(1, "Password is required")
185 | });
186 | 
187 | // Schema for doctype
188 | export const DoctypeSchema = z.string().min(1, "DocType is required");
189 | 
190 | // Schema for document name
191 | export const DocumentNameSchema = z.string().min(1, "Document name is required");
192 | 
193 | // Schema for document data
194 | export const DocumentDataSchema = z.record(z.unknown());
195 | 
196 | // Schema for document filters
197 | export const FiltersSchema = z.record(z.unknown()).optional();
198 | 
199 | // Schema for field list
200 | export const FieldsSchema = z.array(z.string()).optional();
201 | 
202 | // Schema for pagination
203 | export const PaginationSchema = z.object({
204 |   limit: z.number().positive().optional(),
205 |   page: z.number().positive().optional()
206 | });
207 | 
208 | // Schema for report
209 | export const ReportSchema = z.object({
210 |   report_name: z.string().min(1, "Report name is required"),
211 |   filters: z.record(z.unknown()).optional()
212 | });
213 | 
214 | /**
215 |  * Validates input against a schema and returns the validated data
216 |  * or throws an error if validation fails
217 |  */
218 | export function validateInput<T>(schema: z.Schema<T>, data: unknown): T {
219 |   try {
220 |     return schema.parse(data);
221 |   } catch (error) {
222 |     if (error instanceof z.ZodError) {
223 |       // Convert zod error to a more user-friendly format
224 |       const issues = error.errors.map(err => `${err.path.join('.')}: ${err.message}`).join(", ");
225 |       throw new Error(`Validation error: ${issues}`);
226 |     }
227 |     throw error;
228 |   }
229 | }
230 | 
231 | /**
232 |  * Sanitizes string input to prevent injection attacks
233 |  */
234 | export function sanitizeString(input: string): string {
235 |   // Basic sanitization to prevent script injection
236 |   return input
237 |     .replace(/</g, '&lt;')
238 |     .replace(/>/g, '&gt;')
239 |     .replace(/"/g, '&quot;')
240 |     .replace(/'/g, '&#039;');
241 | }
242 | 
243 | /**
244 |  * Sanitizes an object by applying string sanitization to all string properties
245 |  */
246 | export function sanitizeObject(obj: Record<string, any>): Record<string, any> {
247 |   const result: Record<string, any> = {};
248 |   
249 |   Object.entries(obj).forEach(([key, value]) => {
250 |     if (typeof value === 'string') {
251 |       result[key] = sanitizeString(value);
252 |     } else if (value && typeof value === 'object' && !Array.isArray(value)) {
253 |       result[key] = sanitizeObject(value);
254 |     } else {
255 |       result[key] = value;
256 |     }
257 |   });
258 |   
259 |   return result;
260 | }
261 | ```
262 | 
263 | ### 3. Secure Configuration Management
264 | 
265 | Enhance the configuration module with better security practices:
266 | 
267 | ```typescript
268 | // src/utils/config.ts
269 | import fs from 'fs';
270 | import path from 'path';
271 | import dotenv from 'dotenv';
272 | 
273 | export class Config {
274 |   private config: Record<string, string> = {};
275 |   
276 |   constructor(configPath?: string) {
277 |     // Load environment variables from .env file if it exists
278 |     if (configPath && fs.existsSync(configPath)) {
279 |       const envConfig = dotenv.parse(fs.readFileSync(configPath));
280 |       this.config = { ...this.config, ...envConfig };
281 |     }
282 |     
283 |     // Override with actual environment variables
284 |     Object.assign(this.config, process.env);
285 |     
286 |     // Validate required configuration
287 |     this.validateConfig();
288 |   }
289 |   
290 |   private validateConfig() {
291 |     // Validate required configuration
292 |     const requiredVars = ['ERPNEXT_URL'];
293 |     const missing = requiredVars.filter(key => !this.get(key));
294 |     
295 |     if (missing.length > 0) {
296 |       throw new Error(`Missing required configuration: ${missing.join(', ')}`);
297 |     }
298 |     
299 |     // Validate URL format
300 |     try {
301 |       new URL(this.getERPNextUrl());
302 |     } catch (error) {
303 |       throw new Error(`Invalid ERPNEXT_URL: ${this.getERPNextUrl()}`);
304 |     }
305 |     
306 |     // Validate API key and secret (both or neither)
307 |     const hasApiKey = !!this.get('ERPNEXT_API_KEY');
308 |     const hasApiSecret = !!this.get('ERPNEXT_API_SECRET');
309 |     
310 |     if ((hasApiKey && !hasApiSecret) || (!hasApiKey && hasApiSecret)) {
311 |       throw new Error('Both ERPNEXT_API_KEY and ERPNEXT_API_SECRET must be provided if using API key authentication');
312 |     }
313 |   }
314 |   
315 |   /**
316 |    * Get a configuration value
317 |    */
318 |   get(key: string): string | undefined {
319 |     return this.config[key];
320 |   }
321 |   
322 |   /**
323 |    * Get a configuration value or throw if not found
324 |    */
325 |   getRequired(key: string): string {
326 |     const value = this.get(key);
327 |     if (value === undefined) {
328 |       throw new Error(`Required configuration "${key}" not found`);
329 |     }
330 |     return value;
331 |   }
332 |   
333 |   /**
334 |    * Get ERPNext URL, ensuring it doesn't end with a trailing slash
335 |    */
336 |   getERPNextUrl(): string {
337 |     return this.getRequired('ERPNEXT_URL').replace(/\/$/, '');
338 |   }
339 |   
340 |   /**
341 |    * Get ERPNext API key
342 |    */
343 |   getERPNextApiKey(): string | undefined {
344 |     return this.get('ERPNEXT_API_KEY');
345 |   }
346 |   
347 |   /**
348 |    * Get ERPNext API secret
349 |    */
350 |   getERPNextApiSecret(): string | undefined {
351 |     return this.get('ERPNEXT_API_SECRET');
352 |   }
353 |   
354 |   /**
355 |    * Get log level (defaults to "info")
356 |    */
357 |   getLogLevel(): string {
358 |     return this.get('LOG_LEVEL') || 'info';
359 |   }
360 | }
361 | ```
362 | 
363 | ### 4. Security Middleware
364 | 
365 | Add security middleware to protect against common vulnerabilities:
366 | 
367 | ```typescript
368 | // src/middleware/security.ts
369 | import { Logger } from "../utils/logger";
370 | 
371 | export interface RateLimitConfig {
372 |   windowMs: number;
373 |   maxRequests: number;
374 | }
375 | 
376 | /**
377 |  * Simple rate limiting implementation for tools
378 |  */
379 | export class RateLimiter {
380 |   private windowMs: number;
381 |   private maxRequests: number;
382 |   private requests: Map<string, number[]> = new Map();
383 |   private logger: Logger;
384 |   
385 |   constructor(config: RateLimitConfig, logger: Logger) {
386 |     this.windowMs = config.windowMs;
387 |     this.maxRequests = config.maxRequests;
388 |     this.logger = logger;
389 |   }
390 |   
391 |   /**
392 |    * Check if a request should be rate limited
393 |    * @param key Identifier for the rate limit bucket (e.g. tool name, user ID)
394 |    * @returns Whether the request should be allowed
395 |    */
396 |   allowRequest(key: string): boolean {
397 |     const now = Date.now();
398 |     
399 |     // Get existing timestamps for this key
400 |     let timestamps = this.requests.get(key) || [];
401 |     
402 |     // Filter out timestamps outside the current window
403 |     timestamps = timestamps.filter(time => now - time < this.windowMs);
404 |     
405 |     // Check if we've exceeded the limit
406 |     if (timestamps.length >= this.maxRequests) {
407 |       this.logger.warn(`Rate limit exceeded for ${key}`);
408 |       return false;
409 |     }
410 |     
411 |     // Add the new timestamp and update the map
412 |     timestamps.push(now);
413 |     this.requests.set(key, timestamps);
414 |     
415 |     return true;
416 |   }
417 |   
418 |   /**
419 |    * Reset rate limit for a specific key
420 |    */
421 |   reset(key: string): void {
422 |     this.requests.delete(key);
423 |   }
424 | }
425 | 
426 | /**
427 |  * Security headers to add to responses
428 |  */
429 | export const securityHeaders = {
430 |   'X-Content-Type-Options': 'nosniff',
431 |   'X-Frame-Options': 'DENY',
432 |   'Content-Security-Policy': "default-src 'none'",
433 |   'X-XSS-Protection': '1; mode=block',
434 |   'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
435 | };
436 | ```
437 | 
438 | ### 5. Secure Resource and Tool Handlers
439 | 
440 | Update the tool handlers to include validation, rate limiting, and better credential handling:
441 | 
442 | ```typescript
443 | // src/handlers/tool-handlers.ts
444 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
445 | import {
446 |   CallToolRequestSchema,
447 |   ListToolsRequestSchema,
448 |   McpError,
449 |   ErrorCode
450 | } from "@modelcontextprotocol/sdk/types.js";
451 | import { ERPNextClient } from "../client/erpnext-client";
452 | import { AuthManager } from "../auth/authenticator";
453 | import { Logger } from "../utils/logger";
454 | import { handleToolError } from "../utils/error-handler";
455 | import { RateLimiter } from "../middleware/security";
456 | import * as validation from "../utils/validation";
457 | import { RequestContextManager } from "../middleware/context";
458 | 
459 | export function registerToolHandlers(
460 |   server: Server, 
461 |   erpnext: ERPNextClient,
462 |   auth: AuthManager,
463 |   logger: Logger
464 | ) {
465 |   // Create rate limiter for authentication attempts
466 |   const authRateLimiter = new RateLimiter({
467 |     windowMs: 60 * 1000, // 1 minute
468 |     maxRequests: 5 // 5 attempts per minute
469 |   }, logger);
470 |   
471 |   // Create rate limiter for other operations
472 |   const toolRateLimiter = new RateLimiter({
473 |     windowMs: 60 * 1000, // 1 minute
474 |     maxRequests: 30 // 30 requests per minute
475 |   }, logger);
476 | 
477 |   // Handler for listing tools
478 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
479 |     logger.debug("Handling ListToolsRequest");
480 |     
481 |     return {
482 |       tools: [
483 |         {
484 |           name: "get_doctypes",
485 |           description: "Get a list of all available DocTypes",
486 |           inputSchema: {
487 |             type: "object",
488 |             properties: {}
489 |           }
490 |         },
491 |         {
492 |           name: "authenticate_erpnext",
493 |           description: "Authenticate with ERPNext using username and password",
494 |           inputSchema: {
495 |             type: "object",
496 |             properties: {
497 |               username: {
498 |                 type: "string",
499 |                 description: "ERPNext username"
500 |               },
501 |               password: {
502 |                 type: "string",
503 |                 description: "ERPNext password"
504 |               }
505 |             },
506 |             required: ["username", "password"]
507 |           }
508 |         },
509 |         // Other tools...
510 |       ]
511 |     };
512 |   });
513 | 
514 |   // Handler for tool calls
515 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
516 |     const requestId = request.id;
517 |     const context = RequestContextManager.getContext(requestId);
518 |     const correlationId = context?.correlationId;
519 |     
520 |     logger.debug(`Handling CallToolRequest: ${request.params.name}`, { 
521 |       correlationId,
522 |       tool: request.params.name
523 |     });
524 |     
525 |     try {
526 |       // Apply rate limiting based on tool
527 |       const toolName = request.params.name;
528 |       
529 |       if (toolName === 'authenticate_erpnext') {
530 |         // Use stricter rate limiting for authentication
531 |         if (!authRateLimiter.allowRequest(toolName)) {
532 |           throw new McpError(
533 |             ErrorCode.TooManyRequests,
534 |             "Too many authentication attempts. Please try again later."
535 |           );
536 |         }
537 |       } else {
538 |         // Use standard rate limiting for other tools
539 |         if (!toolRateLimiter.allowRequest(toolName)) {
540 |           throw new McpError(
541 |             ErrorCode.TooManyRequests,
542 |             "Too many requests. Please try again later."
543 |           );
544 |         }
545 |       }
546 |       
547 |       // Handle specific tool requests with proper validation
548 |       switch (toolName) {
549 |         case "authenticate_erpnext": {
550 |           const credentials = validation.validateInput(
551 |             validation.AuthCredentialsSchema, 
552 |             request.params.arguments
553 |           );
554 |           
555 |           try {
556 |             await auth.authenticate(credentials.username, credentials.password);
557 |             
558 |             return {
559 |               content: [{
560 |                 type: "text",
561 |                 text: `Successfully authenticated with ERPNext as ${credentials.username}`
562 |               }]
563 |             };
564 |           } catch (error) {
565 |             // Don't expose details of authentication failures
566 |             return {
567 |               content: [{
568 |                 type: "text",
569 |                 text: "Authentication failed. Please check your credentials and try again."
570 |               }],
571 |               isError: true
572 |             };
573 |           }
574 |         }
575 |         
576 |         case "get_documents": {
577 |           // First check authentication
578 |           if (!auth.isAuthenticated()) {
579 |             throw new McpError(
580 |               ErrorCode.Unauthorized,
581 |               "Not authenticated with ERPNext. Use the authenticate_erpnext tool first."
582 |             );
583 |           }
584 |           
585 |           // Validate doctype
586 |           const doctype = validation.validateInput(
587 |             validation.DoctypeSchema,
588 |             request.params.arguments?.doctype
589 |           );
590 |           
591 |           // Validate optional parameters
592 |           const fields = request.params.arguments?.fields ? 
593 |             validation.validateInput(validation.FieldsSchema, request.params.arguments.fields) : 
594 |             undefined;
595 |           
596 |           const filters = request.params.arguments?.filters ?
597 |             validation.validateInput(validation.FiltersSchema, request.params.arguments.filters) :
598 |             undefined;
599 |           
600 |           const limit = request.params.arguments?.limit as number | undefined;
601 |           
602 |           // Get documents
603 |           const documents = await erpnext.getDocList(doctype, filters, fields, limit);
604 |           
605 |           return {
606 |             content: [{
607 |               type: "text",
608 |               text: JSON.stringify(documents, null, 2)
609 |             }]
610 |           };
611 |         }
612 |         
613 |         // Other tool handlers...
614 |         
615 |         default:
616 |           throw new McpError(
617 |             ErrorCode.MethodNotFound,
618 |             `Unknown tool: ${request.params.name}`
619 |           );
620 |       }
621 |     } catch (error) {
622 |       return handleToolError(error, logger);
623 |     }
624 |   });
625 | }
626 | ```
627 | 
628 | ## Security Best Practices
629 | 
630 | ### 1. Credential Handling
631 | 
632 | - Never log passwords or sensitive information
633 | - Use secure environment variables for storing secrets
634 | - Implement token refresh mechanisms where possible
635 | - Consider using a secrets manager for production deployments
636 | 
637 | ### 2. Input Validation
638 | 
639 | - Validate all inputs with proper schemas
640 | - Sanitize inputs where necessary to prevent injection attacks
641 | - Apply validation early in the request flow
642 | 
643 | ### 3. Rate Limiting
644 | 
645 | - Apply rate limits to prevent abuse
646 | - Use stricter limits for sensitive operations like authentication
647 | - Provide informative feedback when limits are exceeded
648 | 
649 | ### 4. Secure Configuration
650 | 
651 | - Validate configuration at startup
652 | - Support multiple configuration sources (env vars, config files)
653 | - Use a secure method for loading credentials
654 | 
655 | ### 5. Authentication Best Practices
656 | 
657 | - Implement proper token handling
658 | - Support multiple authentication methods
659 | - Add contextual information for authentication events
660 | - Log authentication events (success, failure) without exposing sensitive details
661 | 
662 | ## Implementation Plan
663 | 
664 | 1. Add the validation utility module
665 | 2. Implement the secure configuration module
666 | 3. Create the authentication manager
667 | 4. Add security middleware (rate limiting)
668 | 5. Update tool handlers to use validation and rate limiting
669 | 6. Add security headers to responses
670 | 
671 | These security improvements will help protect the ERPNext MCP server against common security vulnerabilities and provide better protection for user credentials and data.
672 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * ERPNext MCP Server
  5 |  * This server provides integration with the ERPNext/Frappe API, allowing:
  6 |  * - Authentication with ERPNext
  7 |  * - Fetching documents from ERPNext
  8 |  * - Querying lists of documents
  9 |  * - Creating and updating documents
 10 |  * - Running reports
 11 |  */
 12 | 
 13 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
 14 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 15 | import {
 16 |   CallToolRequestSchema,
 17 |   ErrorCode,
 18 |   ListResourcesRequestSchema,
 19 |   ListResourceTemplatesRequestSchema,
 20 |   ListToolsRequestSchema,
 21 |   McpError,
 22 |   ReadResourceRequestSchema
 23 | } from "@modelcontextprotocol/sdk/types.js";
 24 | import axios, { AxiosInstance } from "axios";
 25 | 
 26 | // ERPNext API client configuration
 27 | class ERPNextClient {
 28 |   private baseUrl: string;
 29 |   private axiosInstance: AxiosInstance;
 30 |   private authenticated: boolean = false;
 31 | 
 32 |   constructor() {
 33 |     // Get ERPNext configuration from environment variables
 34 |     this.baseUrl = process.env.ERPNEXT_URL || '';
 35 |     
 36 |     // Validate configuration
 37 |     if (!this.baseUrl) {
 38 |       throw new Error("ERPNEXT_URL environment variable is required");
 39 |     }
 40 |     
 41 |     // Remove trailing slash if present
 42 |     this.baseUrl = this.baseUrl.replace(/\/$/, '');
 43 |     
 44 |     // Initialize axios instance
 45 |     this.axiosInstance = axios.create({
 46 |       baseURL: this.baseUrl,
 47 |       withCredentials: true,
 48 |       headers: {
 49 |         'Content-Type': 'application/json',
 50 |         'Accept': 'application/json'
 51 |       }
 52 |     });
 53 |     
 54 |     // Configure authentication if credentials provided
 55 |     const apiKey = process.env.ERPNEXT_API_KEY;
 56 |     const apiSecret = process.env.ERPNEXT_API_SECRET;
 57 |     
 58 |     if (apiKey && apiSecret) {
 59 |       this.axiosInstance.defaults.headers.common['Authorization'] = 
 60 |         `token ${apiKey}:${apiSecret}`;
 61 |       this.authenticated = true;
 62 |     }
 63 |   }
 64 | 
 65 |   isAuthenticated(): boolean {
 66 |     return this.authenticated;
 67 |   }
 68 | 
 69 |   // Get a document by doctype and name
 70 |   async getDocument(doctype: string, name: string): Promise<any> {
 71 |     try {
 72 |       const response = await this.axiosInstance.get(`/api/resource/${doctype}/${name}`);
 73 |       return response.data.data;
 74 |     } catch (error: any) {
 75 |       throw new Error(`Failed to get ${doctype} ${name}: ${error?.message || 'Unknown error'}`);
 76 |     }
 77 |   }
 78 | 
 79 |   // Get list of documents for a doctype
 80 |   async getDocList(doctype: string, filters?: Record<string, any>, fields?: string[], limit?: number): Promise<any[]> {
 81 |     try {
 82 |       let params: Record<string, any> = {};
 83 |       
 84 |       if (fields && fields.length) {
 85 |         params['fields'] = JSON.stringify(fields);
 86 |       }
 87 |       
 88 |       if (filters) {
 89 |         params['filters'] = JSON.stringify(filters);
 90 |       }
 91 |       
 92 |       if (limit) {
 93 |         params['limit_page_length'] = limit;
 94 |       }
 95 |       
 96 |       const response = await this.axiosInstance.get(`/api/resource/${doctype}`, { params });
 97 |       return response.data.data;
 98 |     } catch (error: any) {
 99 |       throw new Error(`Failed to get ${doctype} list: ${error?.message || 'Unknown error'}`);
100 |     }
101 |   }
102 | 
103 |   // Create a new document
104 |   async createDocument(doctype: string, doc: Record<string, any>): Promise<any> {
105 |     try {
106 |       const response = await this.axiosInstance.post(`/api/resource/${doctype}`, {
107 |         data: doc
108 |       });
109 |       return response.data.data;
110 |     } catch (error: any) {
111 |       throw new Error(`Failed to create ${doctype}: ${error?.message || 'Unknown error'}`);
112 |     }
113 |   }
114 | 
115 |   // Update an existing document
116 |   async updateDocument(doctype: string, name: string, doc: Record<string, any>): Promise<any> {
117 |     try {
118 |       const response = await this.axiosInstance.put(`/api/resource/${doctype}/${name}`, {
119 |         data: doc
120 |       });
121 |       return response.data.data;
122 |     } catch (error: any) {
123 |       throw new Error(`Failed to update ${doctype} ${name}: ${error?.message || 'Unknown error'}`);
124 |     }
125 |   }
126 | 
127 |   // Run a report
128 |   async runReport(reportName: string, filters?: Record<string, any>): Promise<any> {
129 |     try {
130 |       const response = await this.axiosInstance.get(`/api/method/frappe.desk.query_report.run`, {
131 |         params: {
132 |           report_name: reportName,
133 |           filters: filters ? JSON.stringify(filters) : undefined
134 |         }
135 |       });
136 |       return response.data.message;
137 |     } catch (error: any) {
138 |       throw new Error(`Failed to run report ${reportName}: ${error?.message || 'Unknown error'}`);
139 |     }
140 |   }
141 | 
142 |   // Get all available DocTypes
143 |   async getAllDocTypes(): Promise<string[]> {
144 |     try {
145 |       // Use the standard REST API to fetch DocTypes
146 |       const response = await this.axiosInstance.get('/api/resource/DocType', {
147 |         params: {
148 |           fields: JSON.stringify(["name"]),
149 |           limit_page_length: 500 // Get more doctypes at once
150 |         }
151 |       });
152 |       
153 |       if (response.data && response.data.data) {
154 |         return response.data.data.map((item: any) => item.name);
155 |       }
156 |       
157 |       return [];
158 |     } catch (error: any) {
159 |       console.error("Failed to get DocTypes:", error?.message || 'Unknown error');
160 |       
161 |       // Try an alternative approach if the first one fails
162 |       try {
163 |         // Try using the method API to get doctypes
164 |         const altResponse = await this.axiosInstance.get('/api/method/frappe.desk.search.search_link', {
165 |           params: {
166 |             doctype: 'DocType',
167 |             txt: '',
168 |             limit: 500
169 |           }
170 |         });
171 |         
172 |         if (altResponse.data && altResponse.data.results) {
173 |           return altResponse.data.results.map((item: any) => item.value);
174 |         }
175 |         
176 |         return [];
177 |       } catch (altError: any) {
178 |         console.error("Alternative DocType fetch failed:", altError?.message || 'Unknown error');
179 |         
180 |         // Fallback: Return a list of common DocTypes
181 |         return [
182 |           "Customer", "Supplier", "Item", "Sales Order", "Purchase Order",
183 |           "Sales Invoice", "Purchase Invoice", "Employee", "Lead", "Opportunity",
184 |           "Quotation", "Payment Entry", "Journal Entry", "Stock Entry"
185 |         ];
186 |       }
187 |     }
188 |   }
189 | }
190 | 
191 | // Cache for doctype metadata
192 | const doctypeCache = new Map<string, any>();
193 | 
194 | // Initialize ERPNext client
195 | const erpnext = new ERPNextClient();
196 | 
197 | // Create an MCP server with capabilities for resources and tools
198 | const server = new Server(
199 |   {
200 |     name: "erpnext-server",
201 |     version: "0.1.0"
202 |   },
203 |   {
204 |     capabilities: {
205 |       resources: {},
206 |       tools: {}
207 |     }
208 |   }
209 | );
210 | 
211 | /**
212 |  * Handler for listing available ERPNext resources.
213 |  * Exposes DocTypes list as a resource and common doctypes as individual resources.
214 |  */
215 | server.setRequestHandler(ListResourcesRequestSchema, async () => {
216 |   // List of common DocTypes to expose as individual resources
217 |   const commonDoctypes = [
218 |     "Customer",
219 |     "Supplier",
220 |     "Item",
221 |     "Sales Order",
222 |     "Purchase Order",
223 |     "Sales Invoice",
224 |     "Purchase Invoice",
225 |     "Employee"
226 |   ];
227 | 
228 |   const resources = [
229 |     // Add a resource to get all doctypes
230 |     {
231 |       uri: "erpnext://DocTypes",
232 |       name: "All DocTypes",
233 |       mimeType: "application/json",
234 |       description: "List of all available DocTypes in the ERPNext instance"
235 |     }
236 |   ];
237 | 
238 |   return {
239 |     resources
240 |   };
241 | });
242 | 
243 | /**
244 |  * Handler for resource templates.
245 |  * Allows querying ERPNext documents by doctype and name.
246 |  */
247 | server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
248 |   const resourceTemplates = [
249 |     {
250 |       uriTemplate: "erpnext://{doctype}/{name}",
251 |       name: "ERPNext Document",
252 |       mimeType: "application/json",
253 |       description: "Fetch an ERPNext document by doctype and name"
254 |     }
255 |   ];
256 | 
257 |   return { resourceTemplates };
258 | });
259 | 
260 | /**
261 |  * Handler for reading ERPNext resources.
262 |  */
263 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
264 |   if (!erpnext.isAuthenticated()) {
265 |     throw new McpError(
266 |       ErrorCode.InvalidRequest,
267 |       "Not authenticated with ERPNext. Please configure API key authentication."
268 |     );
269 |   }
270 | 
271 |   const uri = request.params.uri;
272 |   let result: any;
273 | 
274 |   // Handle special resource: erpnext://DocTypes (list of all doctypes)
275 |   if (uri === "erpnext://DocTypes") {
276 |     try {
277 |       const doctypes = await erpnext.getAllDocTypes();
278 |       result = { doctypes };
279 |     } catch (error: any) {
280 |       throw new McpError(
281 |         ErrorCode.InternalError,
282 |         `Failed to fetch DocTypes: ${error?.message || 'Unknown error'}`
283 |       );
284 |     }
285 |   } else {
286 |     // Handle document access: erpnext://{doctype}/{name}
287 |     const documentMatch = uri.match(/^erpnext:\/\/([^\/]+)\/(.+)$/);
288 |     if (documentMatch) {
289 |       const doctype = decodeURIComponent(documentMatch[1]);
290 |       const name = decodeURIComponent(documentMatch[2]);
291 |       
292 |       try {
293 |         result = await erpnext.getDocument(doctype, name);
294 |       } catch (error: any) {
295 |         throw new McpError(
296 |           ErrorCode.InvalidRequest,
297 |           `Failed to fetch ${doctype} ${name}: ${error?.message || 'Unknown error'}`
298 |         );
299 |       }
300 |     }
301 |   }
302 | 
303 |   if (!result) {
304 |     throw new McpError(
305 |       ErrorCode.InvalidRequest,
306 |       `Invalid ERPNext resource URI: ${uri}`
307 |     );
308 |   }
309 | 
310 |   return {
311 |     contents: [{
312 |       uri: request.params.uri,
313 |       mimeType: "application/json",
314 |       text: JSON.stringify(result, null, 2)
315 |     }]
316 |   };
317 | });
318 | 
319 | /**
320 |  * Handler that lists available tools.
321 |  */
322 | server.setRequestHandler(ListToolsRequestSchema, async () => {
323 |   return {
324 |     tools: [
325 |       {
326 |         name: "get_doctypes",
327 |         description: "Get a list of all available DocTypes",
328 |         inputSchema: {
329 |           type: "object",
330 |           properties: {}
331 |         }
332 |       },
333 |       {
334 |         name: "get_doctype_fields",
335 |         description: "Get fields list for a specific DocType",
336 |         inputSchema: {
337 |           type: "object",
338 |           properties: {
339 |             doctype: {
340 |               type: "string",
341 |               description: "ERPNext DocType (e.g., Customer, Item)"
342 |             }
343 |           },
344 |             required: ["doctype"]
345 |         }
346 |       },
347 |       {
348 |         name: "get_documents",
349 |         description: "Get a list of documents for a specific doctype",
350 |         inputSchema: {
351 |           type: "object",
352 |           properties: {
353 |             doctype: {
354 |               type: "string",
355 |               description: "ERPNext DocType (e.g., Customer, Item)"
356 |             },
357 |             fields: {
358 |               type: "array",
359 |               items: {
360 |                 type: "string"
361 |               },
362 |               description: "Fields to include (optional)"
363 |             },
364 |             filters: {
365 |               type: "object",
366 |               additionalProperties: true,
367 |               description: "Filters in the format {field: value} (optional)"
368 |             },
369 |             limit: {
370 |               type: "number",
371 |               description: "Maximum number of documents to return (optional)"
372 |             }
373 |           },
374 |           required: ["doctype"]
375 |         }
376 |       },
377 |       {
378 |         name: "create_document",
379 |         description: "Create a new document in ERPNext",
380 |         inputSchema: {
381 |           type: "object",
382 |           properties: {
383 |             doctype: {
384 |               type: "string",
385 |               description: "ERPNext DocType (e.g., Customer, Item)"
386 |             },
387 |             data: {
388 |               type: "object",
389 |               additionalProperties: true,
390 |               description: "Document data"
391 |             }
392 |           },
393 |           required: ["doctype", "data"]
394 |         }
395 |       },
396 |       {
397 |         name: "update_document",
398 |         description: "Update an existing document in ERPNext",
399 |         inputSchema: {
400 |           type: "object",
401 |           properties: {
402 |             doctype: {
403 |               type: "string",
404 |               description: "ERPNext DocType (e.g., Customer, Item)"
405 |             },
406 |             name: {
407 |               type: "string",
408 |               description: "Document name/ID"
409 |             },
410 |             data: {
411 |               type: "object",
412 |               additionalProperties: true,
413 |               description: "Document data to update"
414 |             }
415 |           },
416 |           required: ["doctype", "name", "data"]
417 |         }
418 |       },
419 |       {
420 |         name: "run_report",
421 |         description: "Run an ERPNext report",
422 |         inputSchema: {
423 |           type: "object",
424 |           properties: {
425 |             report_name: {
426 |               type: "string",
427 |               description: "Name of the report"
428 |             },
429 |             filters: {
430 |               type: "object",
431 |               additionalProperties: true,
432 |               description: "Report filters (optional)"
433 |             }
434 |           },
435 |           required: ["report_name"]
436 |         }
437 |       }
438 |     ]
439 |   };
440 | });
441 | 
442 | /**
443 |  * Handler for tool calls.
444 |  */
445 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
446 |   switch (request.params.name) {
447 |     case "get_documents": {
448 |       if (!erpnext.isAuthenticated()) {
449 |         return {
450 |           content: [{
451 |             type: "text",
452 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
453 |           }],
454 |           isError: true
455 |         };
456 |       }
457 |       
458 |       const doctype = String(request.params.arguments?.doctype);
459 |       const fields = request.params.arguments?.fields as string[] | undefined;
460 |       const filters = request.params.arguments?.filters as Record<string, any> | undefined;
461 |       const limit = request.params.arguments?.limit as number | undefined;
462 |       
463 |       if (!doctype) {
464 |         throw new McpError(
465 |           ErrorCode.InvalidParams,
466 |           "Doctype is required"
467 |         );
468 |       }
469 |       
470 |       try {
471 |         const documents = await erpnext.getDocList(doctype, filters, fields, limit);
472 |         return {
473 |           content: [{
474 |             type: "text",
475 |             text: JSON.stringify(documents, null, 2)
476 |           }]
477 |         };
478 |       } catch (error: any) {
479 |         return {
480 |           content: [{
481 |             type: "text",
482 |             text: `Failed to get ${doctype} documents: ${error?.message || 'Unknown error'}`
483 |           }],
484 |           isError: true
485 |         };
486 |       }
487 |     }
488 |     
489 |     case "create_document": {
490 |       if (!erpnext.isAuthenticated()) {
491 |         return {
492 |           content: [{
493 |             type: "text",
494 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
495 |           }],
496 |           isError: true
497 |         };
498 |       }
499 |       
500 |       const doctype = String(request.params.arguments?.doctype);
501 |       const data = request.params.arguments?.data as Record<string, any> | undefined;
502 |       
503 |       if (!doctype || !data) {
504 |         throw new McpError(
505 |           ErrorCode.InvalidParams,
506 |           "Doctype and data are required"
507 |         );
508 |       }
509 |       
510 |       try {
511 |         const result = await erpnext.createDocument(doctype, data);
512 |         return {
513 |           content: [{
514 |             type: "text",
515 |             text: `Created ${doctype}: ${result.name}\n\n${JSON.stringify(result, null, 2)}`
516 |           }]
517 |         };
518 |       } catch (error: any) {
519 |         return {
520 |           content: [{
521 |             type: "text",
522 |             text: `Failed to create ${doctype}: ${error?.message || 'Unknown error'}`
523 |           }],
524 |           isError: true
525 |         };
526 |       }
527 |     }
528 |     
529 |     case "update_document": {
530 |       if (!erpnext.isAuthenticated()) {
531 |         return {
532 |           content: [{
533 |             type: "text",
534 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
535 |           }],
536 |           isError: true
537 |         };
538 |       }
539 |       
540 |       const doctype = String(request.params.arguments?.doctype);
541 |       const name = String(request.params.arguments?.name);
542 |       const data = request.params.arguments?.data as Record<string, any> | undefined;
543 |       
544 |       if (!doctype || !name || !data) {
545 |         throw new McpError(
546 |           ErrorCode.InvalidParams,
547 |           "Doctype, name, and data are required"
548 |         );
549 |       }
550 |       
551 |       try {
552 |         const result = await erpnext.updateDocument(doctype, name, data);
553 |         return {
554 |           content: [{
555 |             type: "text",
556 |             text: `Updated ${doctype} ${name}\n\n${JSON.stringify(result, null, 2)}`
557 |           }]
558 |         };
559 |       } catch (error: any) {
560 |         return {
561 |           content: [{
562 |             type: "text",
563 |             text: `Failed to update ${doctype} ${name}: ${error?.message || 'Unknown error'}`
564 |           }],
565 |           isError: true
566 |         };
567 |       }
568 |     }
569 |     
570 |     case "run_report": {
571 |       if (!erpnext.isAuthenticated()) {
572 |         return {
573 |           content: [{
574 |             type: "text",
575 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
576 |           }],
577 |           isError: true
578 |         };
579 |       }
580 |       
581 |       const reportName = String(request.params.arguments?.report_name);
582 |       const filters = request.params.arguments?.filters as Record<string, any> | undefined;
583 |       
584 |       if (!reportName) {
585 |         throw new McpError(
586 |           ErrorCode.InvalidParams,
587 |           "Report name is required"
588 |         );
589 |       }
590 |       
591 |       try {
592 |         const result = await erpnext.runReport(reportName, filters);
593 |         return {
594 |           content: [{
595 |             type: "text",
596 |             text: JSON.stringify(result, null, 2)
597 |           }]
598 |         };
599 |       } catch (error: any) {
600 |         return {
601 |           content: [{
602 |             type: "text",
603 |             text: `Failed to run report ${reportName}: ${error?.message || 'Unknown error'}`
604 |           }],
605 |           isError: true
606 |         };
607 |       }
608 |     }
609 |     
610 |     case "get_doctype_fields": {
611 |       if (!erpnext.isAuthenticated()) {
612 |         return {
613 |           content: [{
614 |             type: "text",
615 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
616 |           }],
617 |           isError: true
618 |         };
619 |       }
620 |       
621 |       const doctype = String(request.params.arguments?.doctype);
622 |       
623 |       if (!doctype) {
624 |         throw new McpError(
625 |           ErrorCode.InvalidParams,
626 |           "Doctype is required"
627 |         );
628 |       }
629 |       
630 |       try {
631 |         // Get a sample document to understand the fields
632 |         const documents = await erpnext.getDocList(doctype, {}, ["*"], 1);
633 |         
634 |         if (!documents || documents.length === 0) {
635 |           return {
636 |             content: [{
637 |               type: "text",
638 |               text: `No documents found for ${doctype}. Cannot determine fields.`
639 |             }],
640 |             isError: true
641 |           };
642 |         }
643 |         
644 |         // Extract field names from the first document
645 |         const sampleDoc = documents[0];
646 |         const fields = Object.keys(sampleDoc).map(field => ({
647 |           fieldname: field,
648 |           value: typeof sampleDoc[field],
649 |           sample: sampleDoc[field]?.toString()?.substring(0, 50) || null
650 |         }));
651 |         
652 |         return {
653 |           content: [{
654 |             type: "text",
655 |             text: JSON.stringify(fields, null, 2)
656 |           }]
657 |         };
658 |       } catch (error: any) {
659 |         return {
660 |           content: [{
661 |             type: "text",
662 |             text: `Failed to get fields for ${doctype}: ${error?.message || 'Unknown error'}`
663 |           }],
664 |           isError: true
665 |         };
666 |       }
667 |     }
668 |     
669 |     case "get_doctypes": {
670 |       if (!erpnext.isAuthenticated()) {
671 |         return {
672 |           content: [{
673 |             type: "text",
674 |             text: "Not authenticated with ERPNext. Please configure API key authentication."
675 |           }],
676 |           isError: true
677 |         };
678 |       }
679 |       
680 |       try {
681 |         const doctypes = await erpnext.getAllDocTypes();
682 |         return {
683 |           content: [{
684 |             type: "text",
685 |             text: JSON.stringify(doctypes, null, 2)
686 |           }]
687 |         };
688 |       } catch (error: any) {
689 |         return {
690 |           content: [{
691 |             type: "text",
692 |             text: `Failed to get DocTypes: ${error?.message || 'Unknown error'}`
693 |           }],
694 |           isError: true
695 |         };
696 |       }
697 |     }
698 |       
699 |     default:
700 |       throw new McpError(
701 |         ErrorCode.MethodNotFound,
702 |         `Unknown tool: ${request.params.name}`
703 |       );
704 |   }
705 | });
706 | 
707 | /**
708 |  * Start the server using stdio transport.
709 |  */
710 | async function main() {
711 |   const transport = new StdioServerTransport();
712 |   await server.connect(transport);
713 |   console.error('ERPNext MCP server running on stdio');
714 | }
715 | 
716 | main().catch((error) => {
717 |   console.error("Server error:", error);
718 |   process.exit(1);
719 | });
720 | 
```