├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── constants.ts ├── index.ts ├── tools │ ├── BaseTool.ts │ ├── BitcoinPrice.ts │ ├── GetCryptoPrice.ts │ └── ListAssets.ts └── utils │ └── toolLoader.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* 5 | PRIVATE_README.md 6 | vscode 7 | .vscode 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use an official Node.js runtime as a parent image 3 | FROM node:18-alpine AS builder 4 | 5 | # Set the working directory in the container 6 | WORKDIR /app 7 | 8 | # Copy the package.json and package-lock.json to the working directory 9 | COPY package.json package-lock.json ./ 10 | 11 | # Install any needed packages specified in package.json 12 | RUN npm install --ignore-scripts 13 | 14 | # Copy the current directory contents into the container at /app 15 | COPY . . 16 | 17 | # Build the app 18 | RUN npm run build 19 | 20 | # Use a smaller node image for the release 21 | FROM node:18-alpine 22 | 23 | # Set the working directory in the container 24 | WORKDIR /app 25 | 26 | # Copy the built files from the builder 27 | COPY --from=builder /app/build ./build 28 | COPY --from=builder /app/package.json ./ 29 | 30 | # Install only production dependencies 31 | RUN npm install --omit=dev 32 | 33 | # Make port 8080 available to the world outside this container 34 | EXPOSE 8080 35 | 36 | # Run the application 37 | ENTRYPOINT ["node", "build/index.js"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alex Andrushevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coincap MCP 2 | 3 | [![smithery badge](https://smithery.ai/badge/coincap-mcp)](https://smithery.ai/server/coincap-mcp) 4 | 5 | ## What does this server do? 6 | 7 | Allows you to query crypto information from coincap's public API - no API keys or registration required 8 | 9 | ## 🚀 Quick Start 10 | 11 | To get started, add this configuration to your Claude Desktop config file: 12 | 13 | **MacOS**: `~/Library/Application\ Support/Claude/claude_desktop_config.json` 14 | **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` 15 | 16 | ```json 17 | { 18 | "mcpServers": { 19 | "mongodb": { 20 | "command": "npx", 21 | "args": ["coincap-mcp"] 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | ### Installing via Smithery 28 | 29 | To install Coincap for Claude Desktop automatically via [Smithery](https://smithery.ai/server/coincap-mcp): 30 | 31 | ```bash 32 | npx -y @smithery/cli install coincap-mcp --client claude 33 | ``` 34 | 35 | ### Prerequisites 36 | 37 | - Node.js 18+ 38 | - npx 39 | 40 | Then, launch Claude Desktop and you're ready to go! 41 | 42 | ## Sample Prompts 43 | 44 | - What is the price of bitcoin? 45 | - What are the available crypto assets? 46 | - What is the market cap of ethereum? 47 | 48 | ## Tools 49 | 50 | #### Bitcoin Price Tool 51 | 52 | Gets price for Bitcoin specifically, it's a simple example of a primitive API call tool 53 | 54 | #### Get Crypto Price Tool 55 | 56 | Gets price for any cryptocurrency available on coincap API. It's a good example of how to get mandatory parameter data for your tool calls 57 | 58 | #### List Assets 59 | 60 | Gets a list of all crypto assets available in coincap API 61 | 62 | ## Development - local build 63 | 64 | To build it locally: 65 | 66 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 67 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 68 | 69 | ```json 70 | { 71 | "mcpServers": { 72 | "coincap-mcp": { 73 | "command": "/path/to/coincap-mcp/build/index.js" 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | ## Development 80 | 81 | Install dependencies: 82 | 83 | ```bash 84 | npm install 85 | ``` 86 | 87 | Build the server: 88 | 89 | ```bash 90 | npm run build 91 | ``` 92 | 93 | For development with auto-rebuild: 94 | 95 | ```bash 96 | npm run watch 97 | ``` 98 | 99 | ## 📜 License 100 | 101 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 102 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coincap-mcp", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "coincap-mcp", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "0.6.0", 12 | "zod": "^3.23.8" 13 | }, 14 | "bin": { 15 | "coincap-mcp": "build/index.js" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.11.24", 19 | "typescript": "^5.3.3" 20 | } 21 | }, 22 | "node_modules/@modelcontextprotocol/sdk": { 23 | "version": "0.6.0", 24 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", 25 | "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", 26 | "dependencies": { 27 | "content-type": "^1.0.5", 28 | "raw-body": "^3.0.0", 29 | "zod": "^3.23.8" 30 | } 31 | }, 32 | "node_modules/@types/node": { 33 | "version": "20.17.9", 34 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", 35 | "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", 36 | "dev": true, 37 | "dependencies": { 38 | "undici-types": "~6.19.2" 39 | } 40 | }, 41 | "node_modules/bytes": { 42 | "version": "3.1.2", 43 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 44 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 45 | "engines": { 46 | "node": ">= 0.8" 47 | } 48 | }, 49 | "node_modules/content-type": { 50 | "version": "1.0.5", 51 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 52 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 53 | "engines": { 54 | "node": ">= 0.6" 55 | } 56 | }, 57 | "node_modules/depd": { 58 | "version": "2.0.0", 59 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 60 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 61 | "engines": { 62 | "node": ">= 0.8" 63 | } 64 | }, 65 | "node_modules/http-errors": { 66 | "version": "2.0.0", 67 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 68 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 69 | "dependencies": { 70 | "depd": "2.0.0", 71 | "inherits": "2.0.4", 72 | "setprototypeof": "1.2.0", 73 | "statuses": "2.0.1", 74 | "toidentifier": "1.0.1" 75 | }, 76 | "engines": { 77 | "node": ">= 0.8" 78 | } 79 | }, 80 | "node_modules/iconv-lite": { 81 | "version": "0.6.3", 82 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 83 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 84 | "dependencies": { 85 | "safer-buffer": ">= 2.1.2 < 3.0.0" 86 | }, 87 | "engines": { 88 | "node": ">=0.10.0" 89 | } 90 | }, 91 | "node_modules/inherits": { 92 | "version": "2.0.4", 93 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 94 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 95 | }, 96 | "node_modules/raw-body": { 97 | "version": "3.0.0", 98 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 99 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 100 | "dependencies": { 101 | "bytes": "3.1.2", 102 | "http-errors": "2.0.0", 103 | "iconv-lite": "0.6.3", 104 | "unpipe": "1.0.0" 105 | }, 106 | "engines": { 107 | "node": ">= 0.8" 108 | } 109 | }, 110 | "node_modules/safer-buffer": { 111 | "version": "2.1.2", 112 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 113 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 114 | }, 115 | "node_modules/setprototypeof": { 116 | "version": "1.2.0", 117 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 118 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 119 | }, 120 | "node_modules/statuses": { 121 | "version": "2.0.1", 122 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 123 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 124 | "engines": { 125 | "node": ">= 0.8" 126 | } 127 | }, 128 | "node_modules/toidentifier": { 129 | "version": "1.0.1", 130 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 131 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 132 | "engines": { 133 | "node": ">=0.6" 134 | } 135 | }, 136 | "node_modules/typescript": { 137 | "version": "5.7.2", 138 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 139 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 140 | "dev": true, 141 | "bin": { 142 | "tsc": "bin/tsc", 143 | "tsserver": "bin/tsserver" 144 | }, 145 | "engines": { 146 | "node": ">=14.17" 147 | } 148 | }, 149 | "node_modules/undici-types": { 150 | "version": "6.19.8", 151 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 152 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 153 | "dev": true 154 | }, 155 | "node_modules/unpipe": { 156 | "version": "1.0.0", 157 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 158 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 159 | "engines": { 160 | "node": ">= 0.8" 161 | } 162 | }, 163 | "node_modules/zod": { 164 | "version": "3.23.8", 165 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", 166 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", 167 | "funding": { 168 | "url": "https://github.com/sponsors/colinhacks" 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coincap-mcp", 3 | "version": "0.9.3", 4 | "author": { 5 | "name": "Alex Andru", 6 | "email": "alex007d@gmail.com" 7 | }, 8 | "description": "A MCP server to get crypto prices from coincap", 9 | "license": "MIT", 10 | "type": "module", 11 | "bin": { 12 | "coincap-mcp": "build/index.js" 13 | }, 14 | "main": "build/index.js", 15 | "files": [ 16 | "build", 17 | "build/**/*" 18 | ], 19 | "scripts": { 20 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 21 | "prepare": "npm run build", 22 | "watch": "tsc --watch", 23 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 24 | "prepack": "npm run build" 25 | }, 26 | "keywords": [ 27 | "mcp", 28 | "claude", 29 | "coincap", 30 | "anthropic", 31 | "ai", 32 | "cryptocurrency" 33 | ], 34 | "dependencies": { 35 | "@modelcontextprotocol/sdk": "0.6.0", 36 | "zod": "^3.23.8" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^20.11.24", 40 | "typescript": "^5.3.3" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/QuantGeekDev/coincap-mcp.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/QuantGeekDev/coincap-mcp/issues" 51 | }, 52 | "engines": { 53 | "node": ">=18" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: [] 9 | properties: {} 10 | commandFunction: 11 | # A function that produces the CLI command to start the MCP on stdio. 12 | |- 13 | (config) => ({ command: 'npx', args: ['coincap-mcp'] }) 14 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const BITCOIN_PRICE_URL = "https://api.coincap.io/v2/assets/bitcoin"; 2 | export const CONSTANTS = { 3 | CRYPTO_PRICE_URL: "https://api.coincap.io/v2/assets/", 4 | PROJECT_NAME: "coincap-mcp", 5 | PROJECT_VERSION: "0.9.2", 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { CONSTANTS } from "./constants.js"; 10 | import { loadTools, createToolsMap } from "./utils/toolLoader.js"; 11 | 12 | const { PROJECT_NAME, PROJECT_VERSION } = CONSTANTS; 13 | 14 | let toolsMap: Map; 15 | 16 | const server = new Server( 17 | { 18 | name: PROJECT_NAME, 19 | version: PROJECT_VERSION, 20 | }, 21 | { 22 | capabilities: { 23 | tools: { 24 | enabled: true, 25 | }, 26 | }, 27 | } 28 | ); 29 | 30 | server.setRequestHandler(ListToolsRequestSchema, async () => { 31 | if (!toolsMap || toolsMap.size === 0) { 32 | console.warn("No tools available for listing"); 33 | return { tools: [] }; 34 | } 35 | return { 36 | tools: Array.from(toolsMap.values()).map((tool) => tool.toolDefinition), 37 | }; 38 | }); 39 | 40 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 41 | if (!toolsMap) { 42 | throw new Error("Tools not initialized"); 43 | } 44 | 45 | const tool = toolsMap.get(request.params.name); 46 | if (!tool) { 47 | throw new Error( 48 | `Unknown tool: ${request.params.name}. Available tools: ${Array.from( 49 | toolsMap.keys() 50 | ).join(", ")}` 51 | ); 52 | } 53 | return tool.toolCall(request); 54 | }); 55 | 56 | async function main() { 57 | try { 58 | console.log("Starting Coincap MCP Server..."); 59 | 60 | const tools = await loadTools(); 61 | if (tools.length === 0) { 62 | console.error("No tools were loaded! Server may not function correctly."); 63 | } 64 | 65 | toolsMap = createToolsMap(tools); 66 | console.log( 67 | `Initialized with ${tools.length} tools:`, 68 | Array.from(toolsMap.keys()).join(", ") 69 | ); 70 | 71 | const transport = new StdioServerTransport(); 72 | console.log("Connecting to transport..."); 73 | await server.connect(transport); 74 | console.log("Server started successfully!"); 75 | } catch (error) { 76 | console.error("Fatal error during server initialization:", error); 77 | process.exit(1); 78 | } 79 | } 80 | 81 | process.on("unhandledRejection", (error) => { 82 | console.error("Unhandled promise rejection:", error); 83 | }); 84 | 85 | main().catch((error) => { 86 | console.error("Server error:", error); 87 | process.exit(1); 88 | }); 89 | -------------------------------------------------------------------------------- /src/tools/BaseTool.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallToolRequestSchema, 3 | Tool, 4 | } from "@modelcontextprotocol/sdk/types.js"; 5 | import { z } from "zod"; 6 | 7 | export interface BaseTool { 8 | name: string; 9 | toolDefinition: Tool; 10 | toolCall(request: z.infer): Promise; 11 | } 12 | 13 | export abstract class BaseToolImplementation implements BaseTool { 14 | abstract name: string; 15 | abstract toolDefinition: Tool; 16 | abstract toolCall( 17 | request: z.infer 18 | ): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/tools/BitcoinPrice.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { BITCOIN_PRICE_URL } from "../constants.js"; 3 | import { BaseToolImplementation } from "./BaseTool.js"; 4 | 5 | class BitcoinPriceTool extends BaseToolImplementation { 6 | name = "bitcoin_price"; 7 | toolDefinition: Tool = { 8 | name: this.name, 9 | description: "Get realtime bitcoin price", 10 | inputSchema: { 11 | type: "object", 12 | }, 13 | }; 14 | 15 | toolCall = async () => { 16 | try { 17 | const response = await fetch(BITCOIN_PRICE_URL); 18 | if (!response.ok) { 19 | throw new Error("Error fetching coincap data"); 20 | } 21 | 22 | const body = await response.json(); 23 | 24 | return { 25 | content: [{ type: "text", text: `${JSON.stringify(body.data)}` }], 26 | }; 27 | } catch (error) { 28 | return { 29 | content: [ 30 | { type: "error", text: JSON.stringify((error as any).message) }, 31 | ], 32 | }; 33 | } 34 | }; 35 | } 36 | 37 | export default BitcoinPriceTool; 38 | -------------------------------------------------------------------------------- /src/tools/GetCryptoPrice.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { CONSTANTS } from "../constants.js"; 3 | import { z } from "zod"; 4 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 5 | import { BaseToolImplementation } from "./BaseTool.js"; 6 | 7 | class GetCryptoPrice extends BaseToolImplementation { 8 | name = "get_crypto_price"; 9 | toolDefinition: Tool = { 10 | name: this.name, 11 | description: "Get realtime crypto price on crypto", 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | name: { 16 | type: "string", 17 | description: "Name of the crypto coin", 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | async toolCall(request: z.infer) { 24 | try { 25 | const cryptoName = request.params.arguments?.name; 26 | if (!cryptoName) { 27 | throw new Error("Missing crypto name"); 28 | } 29 | const url = CONSTANTS.CRYPTO_PRICE_URL + cryptoName; 30 | 31 | const response = await fetch(url); 32 | if (!response.ok) { 33 | throw new Error("Error fetching coincap data"); 34 | } 35 | 36 | const body = await response.json(); 37 | 38 | return { 39 | content: [{ type: "text", text: JSON.stringify(body.data) }], 40 | }; 41 | } catch (error) { 42 | return { 43 | content: [ 44 | { type: "error", text: JSON.stringify((error as any).message) }, 45 | ], 46 | }; 47 | } 48 | } 49 | } 50 | 51 | export default GetCryptoPrice; 52 | -------------------------------------------------------------------------------- /src/tools/ListAssets.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js"; 2 | import { CONSTANTS } from "../constants.js"; 3 | import { BaseToolImplementation } from "./BaseTool.js"; 4 | 5 | class ListAssetsTool extends BaseToolImplementation { 6 | name = "list_assets"; 7 | toolDefinition: Tool = { 8 | name: this.name, 9 | description: "Get all available crypto assets", 10 | inputSchema: { 11 | type: "object", 12 | }, 13 | }; 14 | 15 | toolCall = async () => { 16 | try { 17 | const url = CONSTANTS.CRYPTO_PRICE_URL; 18 | 19 | const response = await fetch(url); 20 | if (!response.ok) { 21 | throw new Error("Error fetching coincap data"); 22 | } 23 | 24 | const body = await response.json(); 25 | 26 | return { 27 | content: [{ type: "text", text: JSON.stringify(body.data) }], 28 | }; 29 | } catch (error) { 30 | return { 31 | content: [ 32 | { type: "error", text: JSON.stringify((error as any).message) }, 33 | ], 34 | }; 35 | } 36 | }; 37 | } 38 | 39 | export default ListAssetsTool; 40 | -------------------------------------------------------------------------------- /src/utils/toolLoader.ts: -------------------------------------------------------------------------------- 1 | import { BaseTool } from "../tools/BaseTool.js"; 2 | import { fileURLToPath } from "url"; 3 | import { dirname, join } from "path"; 4 | import { promises as fs } from "fs"; 5 | 6 | async function findToolsPath(): Promise { 7 | const currentFilePath = fileURLToPath(import.meta.url); 8 | const currentDir = dirname(currentFilePath); 9 | 10 | const possiblePaths = [ 11 | join(currentDir, "..", "tools"), 12 | join(currentDir, "..", "..", "build", "tools"), 13 | join(dirname(dirname(currentDir)), "tools"), 14 | join(dirname(dirname(dirname(currentDir))), "build", "tools"), 15 | join(process.cwd(), "build", "tools"), 16 | ]; 17 | 18 | for (const path of possiblePaths) { 19 | try { 20 | const stats = await fs.stat(path); 21 | if (stats.isDirectory()) { 22 | const files = await fs.readdir(path); 23 | if ( 24 | files.some( 25 | (file) => file.endsWith(".js") && !file.includes("BaseTool") 26 | ) 27 | ) { 28 | return path; 29 | } 30 | } 31 | } catch { 32 | continue; 33 | } 34 | } 35 | 36 | throw new Error("Could not find tools directory"); 37 | } 38 | 39 | const isToolFile = (file: string): boolean => { 40 | return ( 41 | file.endsWith(".js") && 42 | !file.includes("BaseTool") && 43 | !file.includes("index") && 44 | !file.endsWith(".test.js") && 45 | !file.endsWith(".spec.js") && 46 | !file.endsWith(".d.js") 47 | ); 48 | }; 49 | 50 | export async function loadTools(): Promise { 51 | try { 52 | const toolsPath = await findToolsPath(); 53 | const files = await fs.readdir(toolsPath); 54 | const tools: BaseTool[] = []; 55 | 56 | for (const file of files) { 57 | if (!isToolFile(file)) { 58 | continue; 59 | } 60 | 61 | try { 62 | const modulePath = `file://${join(toolsPath, file)}`; 63 | const { default: ToolClass } = await import(modulePath); 64 | 65 | if (!ToolClass) { 66 | continue; 67 | } 68 | 69 | const tool = new ToolClass(); 70 | 71 | if ( 72 | tool.name && 73 | tool.toolDefinition && 74 | typeof tool.toolCall === "function" 75 | ) { 76 | tools.push(tool); 77 | } 78 | } catch (error) { 79 | console.error(`Error loading tool from ${file}:`, error); 80 | } 81 | } 82 | 83 | return tools; 84 | } catch (error) { 85 | console.error(`Failed to load tools:`, error); 86 | return []; 87 | } 88 | } 89 | 90 | export function createToolsMap(tools: BaseTool[]): Map { 91 | return new Map(tools.map((tool) => [tool.name, tool])); 92 | } 93 | -------------------------------------------------------------------------------- /tsconfig.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 | --------------------------------------------------------------------------------