mirror of
https://github.com/aljazceru/mentor-mcp-server.git
synced 2025-12-18 14:34:21 +01:00
feat: Initial implementation of mentor-mcp-server
Implements a Model Context Protocol (MCP) server that provides AI-powered mentorship and feedback tools. Core Features: - TypeScript implementation with ES modules - MCP server setup with stdio transport - Deepseek API integration with rate limiting and retry logic - Secure environment configuration management - Comprehensive utility functions for file and prompt handling Tools: - second-opinion: Provides critical analysis of user requests - Input validation - Rate limiting - Error handling - Sanitized inputs - Structured prompt templates Infrastructure: - Atomic design directory structure - Type-safe implementation - Proper error handling - Security measures (input sanitization, rate limiting) - Development scripts and configuration Security Features: - API key protection - Input sanitization - Rate limiting - Error message sanitization - Secure file path validation This implementation follows MCP best practices and provides a foundation for adding additional tools: - code-review - design-critique - writing-feedback - brainstorm-enhancements
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Deepseek API Configuration
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
DEEPSEEK_API_BASE_URL=https://api.deepseek.com/v1
|
||||
DEEPSEEK_MODEL=deepseek-coder
|
||||
DEEPSEEK_MAX_RETRIES=3
|
||||
DEEPSEEK_TIMEOUT=30000
|
||||
|
||||
# Server Configuration
|
||||
SERVER_NAME=mentor-mcp-server
|
||||
SERVER_VERSION=1.0.0
|
||||
171
.gitignore
vendored
Normal file
171
.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Operating System Files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE and Editor Files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
.tscache/
|
||||
*.js.map
|
||||
*.tgz
|
||||
.npm
|
||||
.eslintcache
|
||||
.rollup.cache
|
||||
*.mjs.map
|
||||
*.cjs.map
|
||||
!*.d.ts.template
|
||||
yarn.lock
|
||||
.pnp.js
|
||||
.pnp.cjs
|
||||
.pnp.mjs
|
||||
.pnp.json
|
||||
.pnp.ts
|
||||
|
||||
# Demo and Example Directories
|
||||
demo/
|
||||
demos/
|
||||
example/
|
||||
examples/
|
||||
samples/
|
||||
.sample-env
|
||||
sample.*
|
||||
!sample.template.*
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Java
|
||||
*.class
|
||||
*.log
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
hs_err_pid*
|
||||
target/
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Ruby
|
||||
*.gem
|
||||
*.rbc
|
||||
/.config
|
||||
/coverage/
|
||||
/InstalledFiles
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/spec/examples.txt
|
||||
/test/tmp/
|
||||
/test/version_tmp/
|
||||
/tmp/
|
||||
.byebug_history
|
||||
|
||||
# Compiled Files
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
|
||||
# Package Files
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# Logs and Databases
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Build and Distribution
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# Documentation
|
||||
docs/_build/
|
||||
doc/api/
|
||||
|
||||
# Dependency Directories
|
||||
jspm_packages/
|
||||
bower_components/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.bak
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.history/
|
||||
329
package-lock.json
generated
Normal file
329
package-lock.json
generated
Normal file
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"name": "mentor-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mentor-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||
"@types/node": "^22.10.10",
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.4.1.tgz",
|
||||
"integrity": "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
|
||||
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz",
|
||||
"integrity": "sha512-YolzkJNxsTL3tCJMWFxpxtG2sCjbZ4LQUBUrkdaJK0ub0p6lmJt+2+1SwhKjLc652lpH9L/79Ptez972H9tphw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
|
||||
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "mentor-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server that provides insightful feedback and guidance on various types of user requests",
|
||||
"type": "module",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x build/index.js",
|
||||
"start": "node build/index.js",
|
||||
"dev": "tsc -w",
|
||||
"clean": "rm -rf build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": ["mcp", "mentor", "feedback", "llm"],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||
"@types/node": "^22.10.10",
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
233
src/api/deepseek/deepseek.ts
Normal file
233
src/api/deepseek/deepseek.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||
import type { LLMResponse } from '../../types/index.js';
|
||||
import { config } from '../../config.js';
|
||||
import { sanitizeInput } from '../../utils/prompt.js';
|
||||
|
||||
/**
|
||||
* Interface for Deepseek API message format
|
||||
*/
|
||||
interface DeepseekMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Deepseek API response format
|
||||
*/
|
||||
interface DeepseekResponse {
|
||||
choices: Array<{
|
||||
message: {
|
||||
content: string;
|
||||
};
|
||||
finish_reason?: string;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for API error response
|
||||
*/
|
||||
interface ApiErrorResponse {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiter implementation using token bucket algorithm
|
||||
*/
|
||||
class RateLimiter {
|
||||
private tokens: number;
|
||||
private readonly maxTokens: number;
|
||||
private readonly refillRate: number;
|
||||
private lastRefill: number;
|
||||
|
||||
constructor(maxTokens: number = 50, refillRate: number = 10) {
|
||||
this.tokens = maxTokens;
|
||||
this.maxTokens = maxTokens;
|
||||
this.refillRate = refillRate; // tokens per second
|
||||
this.lastRefill = Date.now();
|
||||
}
|
||||
|
||||
private refillTokens(): void {
|
||||
const now = Date.now();
|
||||
const timePassed = (now - this.lastRefill) / 1000; // convert to seconds
|
||||
const tokensToAdd = Math.floor(timePassed * this.refillRate);
|
||||
|
||||
this.tokens = Math.min(
|
||||
this.maxTokens,
|
||||
this.tokens + tokensToAdd
|
||||
);
|
||||
this.lastRefill = now;
|
||||
}
|
||||
|
||||
public tryConsume(): boolean {
|
||||
this.refillTokens();
|
||||
if (this.tokens >= 1) {
|
||||
this.tokens -= 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deepseek API client class
|
||||
*/
|
||||
class DeepseekClient {
|
||||
private readonly axiosInstance: AxiosInstance;
|
||||
private readonly rateLimiter: RateLimiter;
|
||||
|
||||
constructor() {
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: config.api.baseUrl,
|
||||
timeout: config.api.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.api.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
this.rateLimiter = new RateLimiter(
|
||||
50, // max 50 requests
|
||||
10 // refill 10 tokens per second
|
||||
);
|
||||
|
||||
// Add response interceptor for error handling
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
this.handleApiError.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles API errors and transforms them into appropriate responses
|
||||
*/
|
||||
private async handleApiError(error: AxiosError<ApiErrorResponse>): Promise<never> {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
const status = error.response.status;
|
||||
const message = error.response.data?.message || error.response.data?.error || error.message;
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
throw new Error('Authentication failed: Invalid API key');
|
||||
case 429:
|
||||
throw new Error('Rate limit exceeded. Please try again later.');
|
||||
case 500:
|
||||
throw new Error('Deepseek API server error. Please try again later.');
|
||||
default:
|
||||
throw new Error(`API error: ${message}`);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
throw new Error('No response received from Deepseek API');
|
||||
} else {
|
||||
// Something happened in setting up the request
|
||||
throw new Error(`Error setting up request: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a call to the Deepseek API with exponential backoff retry
|
||||
*/
|
||||
private async retryWithExponentialBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = config.api.maxRetries,
|
||||
baseDelay: number = 1000
|
||||
): Promise<T> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (error instanceof AxiosError && error.response?.status === 429) {
|
||||
const delay = baseDelay * Math.pow(2, attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Max retries exceeded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a call to the Deepseek API
|
||||
*/
|
||||
public async makeApiCall(
|
||||
prompt: string,
|
||||
systemPrompt: string = 'You are a helpful AI assistant.'
|
||||
): Promise<LLMResponse> {
|
||||
// Check rate limit
|
||||
if (!this.rateLimiter.tryConsume()) {
|
||||
return {
|
||||
text: '',
|
||||
isError: true,
|
||||
errorMessage: 'Rate limit exceeded. Please try again later.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Sanitize inputs
|
||||
const sanitizedPrompt = sanitizeInput(prompt);
|
||||
const sanitizedSystemPrompt = sanitizeInput(systemPrompt);
|
||||
|
||||
const messages: DeepseekMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: sanitizedSystemPrompt,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: sanitizedPrompt,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await this.retryWithExponentialBackoff(async () => {
|
||||
const result = await this.axiosInstance.post<DeepseekResponse>('/chat/completions', {
|
||||
model: config.api.model,
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
return {
|
||||
text: response.data.choices[0].message.content,
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Deepseek API error:', error);
|
||||
return {
|
||||
text: '',
|
||||
isError: true,
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a request can be made (for external rate limit checking)
|
||||
*/
|
||||
public checkRateLimit(): boolean {
|
||||
return this.rateLimiter.tryConsume();
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const deepseekClient = new DeepseekClient();
|
||||
|
||||
// Export the main interface functions
|
||||
export const makeDeepseekAPICall = (prompt: string, systemPrompt?: string) =>
|
||||
deepseekClient.makeApiCall(prompt, systemPrompt);
|
||||
|
||||
export const checkRateLimit = () => deepseekClient.checkRateLimit();
|
||||
27
src/config.ts
Normal file
27
src/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import dotenv from 'dotenv';
|
||||
import type { ServerConfig, APIConfig } from './types/index.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const apiConfig: APIConfig = {
|
||||
apiKey: requireEnv('DEEPSEEK_API_KEY'),
|
||||
baseUrl: process.env.DEEPSEEK_API_BASE_URL || 'https://api.deepseek.com/v1',
|
||||
model: process.env.DEEPSEEK_MODEL || 'deepseek-coder',
|
||||
maxRetries: parseInt(process.env.DEEPSEEK_MAX_RETRIES || '3', 10),
|
||||
timeout: parseInt(process.env.DEEPSEEK_TIMEOUT || '30000', 10),
|
||||
};
|
||||
|
||||
export const config: ServerConfig = {
|
||||
serverName: process.env.SERVER_NAME || 'mentor-mcp-server',
|
||||
serverVersion: process.env.SERVER_VERSION || '1.0.0',
|
||||
api: apiConfig,
|
||||
};
|
||||
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import './server.js';
|
||||
183
src/server.ts
Normal file
183
src/server.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
McpError,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { config } from "./config.js";
|
||||
import * as secondOpinion from "./tools/second-opinion/index.js";
|
||||
import { isValidToolArgs, SecondOpinionArgs } from "./types/index.js";
|
||||
|
||||
/**
|
||||
* MentorServer class implements an MCP server that provides mentorship and feedback tools.
|
||||
* It uses the Deepseek API to generate insightful responses for various types of requests.
|
||||
*/
|
||||
class MentorServer {
|
||||
private server: Server;
|
||||
private isShuttingDown: boolean = false;
|
||||
|
||||
constructor() {
|
||||
// Initialize the MCP server with basic configuration
|
||||
this.server = new Server(
|
||||
{
|
||||
name: config.serverName,
|
||||
version: config.serverVersion,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupRequestHandlers();
|
||||
this.setupErrorHandler();
|
||||
this.setupSignalHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up request handlers for the MCP server
|
||||
*/
|
||||
private setupRequestHandlers(): void {
|
||||
// Register tool listing handler
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [secondOpinion.definition],
|
||||
}));
|
||||
|
||||
// Register tool execution handler
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
// Check if server is shutting down
|
||||
if (this.isShuttingDown) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
"Server is shutting down"
|
||||
);
|
||||
}
|
||||
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case "second_opinion": {
|
||||
if (!args || !isValidToolArgs(args, ["user_request"])) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
"Missing required parameter: user_request"
|
||||
);
|
||||
}
|
||||
|
||||
const userRequest = args.user_request;
|
||||
if (typeof userRequest !== 'string') {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
"Parameter 'user_request' must be a string"
|
||||
);
|
||||
}
|
||||
|
||||
const toolArgs: SecondOpinionArgs = {
|
||||
user_request: userRequest
|
||||
};
|
||||
|
||||
return await secondOpinion.handler(toolArgs);
|
||||
}
|
||||
default:
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
`Tool not found: ${name}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle errors that aren't already McpErrors
|
||||
if (!(error instanceof McpError)) {
|
||||
console.error(`Error executing tool ${name}:`, error);
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Internal server error while executing tool ${name}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the error handler for the MCP server
|
||||
*/
|
||||
private setupErrorHandler(): void {
|
||||
this.server.onerror = (error) => {
|
||||
console.error("[MCP Error]", error);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up signal handlers for graceful shutdown
|
||||
*/
|
||||
private setupSignalHandlers(): void {
|
||||
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
|
||||
|
||||
signals.forEach(signal => {
|
||||
process.on(signal, async () => {
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
await this.stop();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', async (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
await this.stop();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the MCP server
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error("Mentor MCP server running on stdio");
|
||||
} catch (error) {
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the MCP server gracefully
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
console.error("Shutting down server...");
|
||||
|
||||
try {
|
||||
await this.server.close();
|
||||
console.error("Server stopped");
|
||||
} catch (error) {
|
||||
console.error("Error during shutdown:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and start the server
|
||||
const server = new MentorServer();
|
||||
server.start().catch((error) => {
|
||||
console.error("Server startup error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
1
src/tools/second-opinion/index.ts
Normal file
1
src/tools/second-opinion/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { definition, handler } from './second-opinion.js';
|
||||
113
src/tools/second-opinion/second-opinion.ts
Normal file
113
src/tools/second-opinion/second-opinion.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { ToolDefinition, SecondOpinionArgs } from '../../types/index.js';
|
||||
import { makeDeepseekAPICall, checkRateLimit } from '../../api/deepseek/deepseek.js';
|
||||
import { createPrompt, PromptTemplate } from '../../utils/prompt.js';
|
||||
|
||||
/**
|
||||
* System prompt for the second opinion tool
|
||||
*/
|
||||
const SYSTEM_PROMPT = `You are an expert mentor providing second opinions on user requests.
|
||||
Your role is to analyze requests and identify critical considerations that might be overlooked.
|
||||
Focus on modern practices, potential pitfalls, and important factors for success.
|
||||
|
||||
Format your response as a clear, non-numbered list of points, focusing on what's most relevant
|
||||
to the specific request. Each point should be concise but informative.`;
|
||||
|
||||
/**
|
||||
* Prompt template for generating second opinions
|
||||
*/
|
||||
const PROMPT_TEMPLATE: PromptTemplate = {
|
||||
template: `User Request: {user_request}
|
||||
|
||||
Task: List the critical considerations for this user request:
|
||||
- Core problem/concept to address
|
||||
- Common pitfalls or edge cases
|
||||
- Security/performance implications (if applicable)
|
||||
- Prerequisites or dependencies
|
||||
- Resource constraints and requirements to consider
|
||||
- Advanced topics that could add value
|
||||
- Maintenance/scalability factors
|
||||
|
||||
Reminder: You are not fulfilling the user request, only generating a plain text, non-numbered list of non-obvious points of consideration.
|
||||
|
||||
Format: Brief, clear points in plain text. Focus on what's most relevant to the specific request.`,
|
||||
systemPrompt: SYSTEM_PROMPT
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool definition for the second opinion tool
|
||||
*/
|
||||
export const definition: ToolDefinition = {
|
||||
name: 'second_opinion',
|
||||
description: 'Provides a second opinion on a user\'s request by analyzing it with an LLM and listing critical considerations.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_request: {
|
||||
type: 'string',
|
||||
description: 'The user\'s original request (e.g., \'Explain Python to me\' or \'Build a login system\')',
|
||||
},
|
||||
},
|
||||
required: ['user_request'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the execution of the second opinion tool
|
||||
*
|
||||
* @param args - Tool arguments containing the user request
|
||||
* @returns Tool response containing the generated second opinion
|
||||
*/
|
||||
export async function handler(args: SecondOpinionArgs) {
|
||||
// Check rate limit first
|
||||
if (!checkRateLimit()) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Rate limit exceeded. Please try again later.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the complete prompt using the template
|
||||
const prompt = createPrompt(PROMPT_TEMPLATE, {
|
||||
user_request: args.user_request
|
||||
});
|
||||
|
||||
// Make the API call
|
||||
const response = await makeDeepseekAPICall(prompt, SYSTEM_PROMPT);
|
||||
|
||||
if (response.isError) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error generating second opinion: ${response.errorMessage || 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Format the response
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `<internal_thoughts>\n${response.text}\n</internal_thoughts>`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Second opinion tool error:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error processing request: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
57
src/types/index.ts
Normal file
57
src/types/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
export interface ToolDefinition extends Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
oneOf?: Array<{ required: string[] }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolContent {
|
||||
type: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ToolResponse {
|
||||
content: ToolContent[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface LLMResponse {
|
||||
text: string;
|
||||
isError: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface FileValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface APIConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
maxRetries: number;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
serverName: string;
|
||||
serverVersion: string;
|
||||
api: APIConfig;
|
||||
}
|
||||
|
||||
export interface SecondOpinionArgs {
|
||||
user_request: string;
|
||||
}
|
||||
|
||||
// Type guard for tool arguments
|
||||
export function isValidToolArgs(args: Record<string, unknown> | undefined, required: string[]): boolean {
|
||||
if (!args) return false;
|
||||
return required.every(key => key in args && args[key] !== undefined);
|
||||
}
|
||||
88
src/utils/file.ts
Normal file
88
src/utils/file.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { FileValidationResult } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Validates a file path to ensure it's safe to access.
|
||||
* Prevents directory traversal attacks and ensures the path is within allowed boundaries.
|
||||
*
|
||||
* @param filePath - The file path to validate
|
||||
* @returns FileValidationResult indicating if the path is valid and any error message
|
||||
*/
|
||||
export function validateFilePath(filePath: string): FileValidationResult {
|
||||
try {
|
||||
// Resolve the absolute path
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
// Get the server's root directory (two levels up from utils)
|
||||
const serverRoot = path.resolve(__dirname, '../../');
|
||||
|
||||
// Check if the path is within the server root
|
||||
if (!resolvedPath.startsWith(serverRoot)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Access denied: Path is outside the server root directory'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'File not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we have read permissions
|
||||
try {
|
||||
fs.accessSync(resolvedPath, fs.constants.R_OK);
|
||||
} catch {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Permission denied: Cannot read file'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely reads the content of a file after validation.
|
||||
*
|
||||
* @param filePath - The path to the file to read
|
||||
* @returns Promise resolving to the file content
|
||||
* @throws Error if file validation fails or reading fails
|
||||
*/
|
||||
export async function readFileContent(filePath: string): Promise<string> {
|
||||
const validation = validateFilePath(filePath);
|
||||
if (!validation.isValid) {
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
|
||||
try {
|
||||
return await fs.promises.readFile(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file exists and is accessible.
|
||||
*
|
||||
* @param filePath - The path to check
|
||||
* @returns boolean indicating if the file exists and is accessible
|
||||
*/
|
||||
export function fileExists(filePath: string): boolean {
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
113
src/utils/prompt.ts
Normal file
113
src/utils/prompt.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Represents a prompt template with placeholders for dynamic values.
|
||||
*/
|
||||
export interface PromptTemplate {
|
||||
template: string;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents variables that can be used in a prompt template.
|
||||
*/
|
||||
export type PromptVariables = Record<string, string | number | boolean>;
|
||||
|
||||
/**
|
||||
* Fills a prompt template with provided variables.
|
||||
*
|
||||
* @param template - The prompt template containing placeholders
|
||||
* @param variables - Object containing values for the placeholders
|
||||
* @returns The filled prompt string
|
||||
* @throws Error if required variables are missing
|
||||
*/
|
||||
export function fillPromptTemplate(template: string, variables: PromptVariables): string {
|
||||
// Find all placeholders in the template
|
||||
const placeholders = template.match(/\{([^}]+)\}/g) || [];
|
||||
|
||||
// Create a map of required variables
|
||||
const requiredVars = new Set(
|
||||
placeholders.map(p => p.slice(1, -1)) // Remove { and }
|
||||
);
|
||||
|
||||
// Check if all required variables are provided
|
||||
const missingVars = Array.from(requiredVars).filter(v => !(v in variables));
|
||||
if (missingVars.length > 0) {
|
||||
throw new Error(`Missing required variables: ${missingVars.join(', ')}`);
|
||||
}
|
||||
|
||||
// Replace all placeholders with their values
|
||||
return template.replace(/\{([^}]+)\}/g, (_, key) => {
|
||||
const value = variables[key];
|
||||
return String(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes user input to prevent prompt injection attacks.
|
||||
*
|
||||
* @param input - The user input to sanitize
|
||||
* @returns Sanitized input string
|
||||
*/
|
||||
export function sanitizeInput(input: string): string {
|
||||
// Remove any attempt to break out of the current context
|
||||
return input
|
||||
.replace(/```/g, '\\`\\`\\`') // Escape code blocks
|
||||
.replace(/\{/g, '\\{') // Escape template literals
|
||||
.replace(/\}/g, '\\}')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete prompt by combining system prompt, template, and variables.
|
||||
*
|
||||
* @param promptTemplate - The prompt template object
|
||||
* @param variables - Variables to fill in the template
|
||||
* @returns Complete prompt string
|
||||
*/
|
||||
export function createPrompt(
|
||||
promptTemplate: PromptTemplate,
|
||||
variables: PromptVariables
|
||||
): string {
|
||||
const filledTemplate = fillPromptTemplate(promptTemplate.template, variables);
|
||||
|
||||
if (promptTemplate.systemPrompt) {
|
||||
return `${promptTemplate.systemPrompt}\n\n${filledTemplate}`;
|
||||
}
|
||||
|
||||
return filledTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a prompt to ensure it doesn't exceed maximum length.
|
||||
*
|
||||
* @param prompt - The prompt to validate
|
||||
* @param maxLength - Maximum allowed length (default: 4000)
|
||||
* @returns boolean indicating if the prompt is valid
|
||||
*/
|
||||
export function validatePrompt(prompt: string, maxLength: number = 4000): boolean {
|
||||
return prompt.length <= maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a prompt to fit within maximum length while preserving meaning.
|
||||
*
|
||||
* @param prompt - The prompt to truncate
|
||||
* @param maxLength - Maximum allowed length
|
||||
* @returns Truncated prompt string
|
||||
*/
|
||||
export function truncatePrompt(prompt: string, maxLength: number = 4000): string {
|
||||
if (prompt.length <= maxLength) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// Try to truncate at a sentence boundary
|
||||
const truncated = prompt.slice(0, maxLength);
|
||||
const lastSentence = truncated.match(/.*[.!?]/);
|
||||
|
||||
if (lastSentence) {
|
||||
return lastSentence[0];
|
||||
}
|
||||
|
||||
// If no sentence boundary found, truncate at last complete word
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
return truncated.slice(0, lastSpace) + '...';
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
||||
Reference in New Issue
Block a user