blixify-charts-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/build/index.js +732 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# metabase-server MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/server/@imlewc/metabase-server)
|
|
4
|
+
|
|
5
|
+
A Model Context Protocol server for Metabase integration.
|
|
6
|
+
|
|
7
|
+
This is a TypeScript-based MCP server that implements integration with Metabase API. It allows AI assistants to interact with Metabase, providing access to:
|
|
8
|
+
|
|
9
|
+
- Dashboards, questions/cards, and databases as resources
|
|
10
|
+
- Tools for listing and executing Metabase queries
|
|
11
|
+
- Ability to view and interact with Metabase data
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### Resources
|
|
16
|
+
- List and access Metabase resources via `metabase://` URIs
|
|
17
|
+
- Access dashboards, cards/questions, and databases
|
|
18
|
+
- JSON content type for structured data access
|
|
19
|
+
|
|
20
|
+
### Tools
|
|
21
|
+
- `list_dashboards` - List all dashboards in Metabase
|
|
22
|
+
- `list_cards` - List all questions/cards in Metabase
|
|
23
|
+
- `list_databases` - List all databases in Metabase
|
|
24
|
+
- `execute_card` - Execute a Metabase question/card and get results
|
|
25
|
+
- `get_dashboard_cards` - Get all cards in a dashboard
|
|
26
|
+
- `execute_query` - Execute a SQL query against a Metabase database
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Before running the server, you need to set environment variables for authentication. The server supports two methods:
|
|
31
|
+
|
|
32
|
+
1. **API Key (Preferred):**
|
|
33
|
+
* `METABASE_URL`: The URL of your Metabase instance (e.g., `https://your-metabase-instance.com`).
|
|
34
|
+
* `METABASE_API_KEY`: Your Metabase API key.
|
|
35
|
+
|
|
36
|
+
2. **Username/Password (Fallback):**
|
|
37
|
+
* `METABASE_URL`: The URL of your Metabase instance.
|
|
38
|
+
* `METABASE_USERNAME`: Your Metabase username.
|
|
39
|
+
* `METABASE_PASSWORD`: Your Metabase password.
|
|
40
|
+
|
|
41
|
+
The server will first check for `METABASE_API_KEY`. If it's set, API key authentication will be used. If `METABASE_API_KEY` is not set, the server will fall back to using `METABASE_USERNAME` and `METABASE_PASSWORD`. You must provide credentials for at least one of these methods.
|
|
42
|
+
|
|
43
|
+
**Example setup:**
|
|
44
|
+
|
|
45
|
+
Using API Key:
|
|
46
|
+
```bash
|
|
47
|
+
# Required environment variables
|
|
48
|
+
export METABASE_URL=https://your-metabase-instance.com
|
|
49
|
+
export METABASE_API_KEY=your_metabase_api_key
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or, using Username/Password:
|
|
53
|
+
```bash
|
|
54
|
+
# Required environment variables
|
|
55
|
+
export METABASE_URL=https://your-metabase-instance.com
|
|
56
|
+
export METABASE_USERNAME=your_username
|
|
57
|
+
export METABASE_PASSWORD=your_password
|
|
58
|
+
```
|
|
59
|
+
You can set these environment variables in your shell profile or use a `.env` file with a package like `dotenv`.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
Install dependencies:
|
|
64
|
+
```bash
|
|
65
|
+
npm install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Build the server:
|
|
69
|
+
```bash
|
|
70
|
+
npm run build
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For development with auto-rebuild:
|
|
74
|
+
```bash
|
|
75
|
+
npm run watch
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
```bash
|
|
80
|
+
# Oneliner, suitable for CI environment
|
|
81
|
+
git clone https://github.com/imlewc/metabase-server.git && cd metabase-server && npm i && npm run build && npm link
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To use with Claude Desktop, add the server config:
|
|
85
|
+
|
|
86
|
+
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
87
|
+
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"metabase-server": {
|
|
93
|
+
"command": "metabase-server",
|
|
94
|
+
"env": {
|
|
95
|
+
"METABASE_URL": "https://your-metabase-instance.com",
|
|
96
|
+
// Use API Key (preferred)
|
|
97
|
+
"METABASE_API_KEY": "your_metabase_api_key"
|
|
98
|
+
// Or Username/Password (if API Key is not set)
|
|
99
|
+
// "METABASE_USERNAME": "your_username",
|
|
100
|
+
// "METABASE_PASSWORD": "your_password"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Note: You can also set these environment variables in your system instead of in the config file if you prefer.
|
|
108
|
+
|
|
109
|
+
### Installing via Smithery
|
|
110
|
+
|
|
111
|
+
To install metabase-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@imlewc/metabase-server):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx -y @smithery/cli install @imlewc/metabase-server --client claude
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Debugging
|
|
118
|
+
|
|
119
|
+
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:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run inspector
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The Inspector will provide a URL to access debugging tools in your browser.
|
|
126
|
+
|
|
127
|
+
## Testing
|
|
128
|
+
|
|
129
|
+
After configuring the environment variables as described in the "Configuration" section, you can manually test the server's authentication. The MCP Inspector (`npm run inspector`) is a useful tool for sending requests to the server.
|
|
130
|
+
|
|
131
|
+
### 1. Testing with API Key Authentication
|
|
132
|
+
|
|
133
|
+
1. Set the `METABASE_URL` and `METABASE_API_KEY` environment variables with your Metabase instance URL and a valid API key.
|
|
134
|
+
2. Ensure `METABASE_USERNAME` and `METABASE_PASSWORD` are unset or leave them, as the API key should take precedence.
|
|
135
|
+
3. Start the server: `npm run build && node build/index.js` (or use your chosen method for running the server, like via Claude Desktop config).
|
|
136
|
+
4. Check the server logs. You should see a message indicating that it's using API key authentication (e.g., "Using Metabase API Key for authentication.").
|
|
137
|
+
5. Using an MCP client or the MCP Inspector, try calling a tool, for example, `tools/call` with `{"name": "list_dashboards"}`.
|
|
138
|
+
6. Verify that the tool call is successful and you receive the expected data.
|
|
139
|
+
|
|
140
|
+
### 2. Testing with Username/Password Authentication (Fallback)
|
|
141
|
+
|
|
142
|
+
1. Ensure the `METABASE_API_KEY` environment variable is unset.
|
|
143
|
+
2. Set `METABASE_URL`, `METABASE_USERNAME`, and `METABASE_PASSWORD` with valid credentials for your Metabase instance.
|
|
144
|
+
3. Start the server.
|
|
145
|
+
4. Check the server logs. You should see a message indicating that it's using username/password authentication (e.g., "Using Metabase username/password for authentication." followed by "Authenticating with Metabase using username/password...").
|
|
146
|
+
5. Using an MCP client or the MCP Inspector, try calling the `list_dashboards` tool.
|
|
147
|
+
6. Verify that the tool call is successful.
|
|
148
|
+
|
|
149
|
+
### 3. Testing Authentication Failures
|
|
150
|
+
|
|
151
|
+
* **Invalid API Key:**
|
|
152
|
+
1. Set `METABASE_URL` and an invalid `METABASE_API_KEY`. Ensure `METABASE_USERNAME` and `METABASE_PASSWORD` variables are unset.
|
|
153
|
+
2. Start the server.
|
|
154
|
+
3. Attempt to call a tool (e.g., `list_dashboards`). The tool call should fail, and the server logs might indicate an authentication error from Metabase (e.g., "Metabase API error: Invalid X-API-Key").
|
|
155
|
+
* **Invalid Username/Password:**
|
|
156
|
+
1. Ensure `METABASE_API_KEY` is unset. Set `METABASE_URL` and invalid `METABASE_USERNAME`/`METABASE_PASSWORD`.
|
|
157
|
+
2. Start the server.
|
|
158
|
+
3. Attempt to call a tool. The tool call should fail due to failed session authentication. The server logs might show "Authentication failed" or "Failed to authenticate with Metabase".
|
|
159
|
+
* **Missing Credentials:**
|
|
160
|
+
1. Unset `METABASE_API_KEY`, `METABASE_USERNAME`, and `METABASE_PASSWORD`. Set only `METABASE_URL`.
|
|
161
|
+
2. Attempt to start the server.
|
|
162
|
+
3. The server should fail to start and log an error message stating that authentication credentials (either API key or username/password) are required (e.g., "Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required").
|
package/build/index.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 为老版本 Node.js 添加 AbortController polyfill
|
|
3
|
+
import AbortController from 'abort-controller';
|
|
4
|
+
global.AbortController = global.AbortController || AbortController;
|
|
5
|
+
/**
|
|
6
|
+
* Metabase MCP 服务器
|
|
7
|
+
* 实现与 Metabase API 的交互,提供以下功能:
|
|
8
|
+
* - 获取仪表板列表
|
|
9
|
+
* - 获取问题列表
|
|
10
|
+
* - 获取数据库列表
|
|
11
|
+
* - 执行问题查询
|
|
12
|
+
* - 获取仪表板详情
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { ListResourcesRequestSchema, ReadResourceRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import axios from "axios";
|
|
19
|
+
// 自定义错误枚举
|
|
20
|
+
var ErrorCode;
|
|
21
|
+
(function (ErrorCode) {
|
|
22
|
+
ErrorCode["InternalError"] = "internal_error";
|
|
23
|
+
ErrorCode["InvalidRequest"] = "invalid_request";
|
|
24
|
+
ErrorCode["InvalidParams"] = "invalid_params";
|
|
25
|
+
ErrorCode["MethodNotFound"] = "method_not_found";
|
|
26
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
27
|
+
// 自定义错误类
|
|
28
|
+
class McpError extends Error {
|
|
29
|
+
code;
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.name = "McpError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// 从环境变量获取 Metabase 配置
|
|
37
|
+
const METABASE_URL = process.env.METABASE_URL;
|
|
38
|
+
const METABASE_USERNAME = process.env.METABASE_USERNAME;
|
|
39
|
+
const METABASE_PASSWORD = process.env.METABASE_PASSWORD;
|
|
40
|
+
const METABASE_API_KEY = process.env.METABASE_API_KEY;
|
|
41
|
+
if (!METABASE_URL || (!METABASE_API_KEY && (!METABASE_USERNAME || !METABASE_PASSWORD))) {
|
|
42
|
+
throw new Error("Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required");
|
|
43
|
+
}
|
|
44
|
+
// 创建自定义 Schema 对象,使用 z.object
|
|
45
|
+
const ListResourceTemplatesRequestSchema = z.object({
|
|
46
|
+
method: z.literal("resources/list_templates")
|
|
47
|
+
});
|
|
48
|
+
const ListToolsRequestSchema = z.object({
|
|
49
|
+
method: z.literal("tools/list")
|
|
50
|
+
});
|
|
51
|
+
class MetabaseServer {
|
|
52
|
+
server;
|
|
53
|
+
axiosInstance;
|
|
54
|
+
sessionToken = null;
|
|
55
|
+
constructor() {
|
|
56
|
+
this.server = new Server({
|
|
57
|
+
name: "metabase-server",
|
|
58
|
+
version: "0.1.0",
|
|
59
|
+
}, {
|
|
60
|
+
capabilities: {
|
|
61
|
+
resources: {},
|
|
62
|
+
tools: {},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
this.axiosInstance = axios.create({
|
|
66
|
+
baseURL: METABASE_URL,
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (METABASE_API_KEY) {
|
|
72
|
+
this.logInfo('Using Metabase API Key for authentication.');
|
|
73
|
+
this.axiosInstance.defaults.headers.common['X-API-Key'] = METABASE_API_KEY;
|
|
74
|
+
this.sessionToken = "api_key_used"; // Indicate API key is in use
|
|
75
|
+
}
|
|
76
|
+
else if (METABASE_USERNAME && METABASE_PASSWORD) {
|
|
77
|
+
this.logInfo('Using Metabase username/password for authentication.');
|
|
78
|
+
// Existing session token logic will apply
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// This case should ideally be caught by the initial environment variable check
|
|
82
|
+
// but as a safeguard:
|
|
83
|
+
this.logError('Metabase authentication credentials not configured properly.', {});
|
|
84
|
+
throw new Error("Metabase authentication credentials not provided or incomplete.");
|
|
85
|
+
}
|
|
86
|
+
this.setupResourceHandlers();
|
|
87
|
+
this.setupToolHandlers();
|
|
88
|
+
// Enhanced error handling with logging
|
|
89
|
+
this.server.onerror = (error) => {
|
|
90
|
+
this.logError('Server Error', error);
|
|
91
|
+
};
|
|
92
|
+
process.on('SIGINT', async () => {
|
|
93
|
+
this.logInfo('Shutting down server...');
|
|
94
|
+
await this.server.close();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Add logging utilities
|
|
99
|
+
logInfo(message, data) {
|
|
100
|
+
const logMessage = {
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
level: 'info',
|
|
103
|
+
message,
|
|
104
|
+
data
|
|
105
|
+
};
|
|
106
|
+
console.error(JSON.stringify(logMessage));
|
|
107
|
+
// MCP SDK changed, can't directly access session
|
|
108
|
+
try {
|
|
109
|
+
// Use current session if available
|
|
110
|
+
console.error(`INFO: ${message}`);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
// Ignore if session not available
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
logError(message, error) {
|
|
117
|
+
const errorObj = error;
|
|
118
|
+
const apiError = error;
|
|
119
|
+
const logMessage = {
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
level: 'error',
|
|
122
|
+
message,
|
|
123
|
+
error: errorObj.message || 'Unknown error',
|
|
124
|
+
stack: errorObj.stack
|
|
125
|
+
};
|
|
126
|
+
console.error(JSON.stringify(logMessage));
|
|
127
|
+
// MCP SDK changed, can't directly access session
|
|
128
|
+
try {
|
|
129
|
+
console.error(`ERROR: ${message} - ${errorObj.message || 'Unknown error'}`);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
// Ignore if session not available
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 获取 Metabase 会话令牌
|
|
137
|
+
*/
|
|
138
|
+
async getSessionToken() {
|
|
139
|
+
if (this.sessionToken) { // Handles both API key ("api_key_used") and actual session tokens
|
|
140
|
+
return this.sessionToken;
|
|
141
|
+
}
|
|
142
|
+
// This part should only be reached if using username/password and sessionToken is null
|
|
143
|
+
this.logInfo('Authenticating with Metabase using username/password...');
|
|
144
|
+
try {
|
|
145
|
+
const response = await this.axiosInstance.post('/api/session', {
|
|
146
|
+
username: METABASE_USERNAME,
|
|
147
|
+
password: METABASE_PASSWORD,
|
|
148
|
+
});
|
|
149
|
+
this.sessionToken = response.data.id;
|
|
150
|
+
// 设置默认请求头
|
|
151
|
+
this.axiosInstance.defaults.headers.common['X-Metabase-Session'] = this.sessionToken;
|
|
152
|
+
this.logInfo('Successfully authenticated with Metabase');
|
|
153
|
+
return this.sessionToken;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
this.logError('Authentication failed', error);
|
|
157
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to authenticate with Metabase');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 设置资源处理程序
|
|
162
|
+
*/
|
|
163
|
+
setupResourceHandlers() {
|
|
164
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
165
|
+
this.logInfo('Listing resources...', { requestStructure: JSON.stringify(request) });
|
|
166
|
+
if (!METABASE_API_KEY) {
|
|
167
|
+
await this.getSessionToken();
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
// 获取仪表板列表
|
|
171
|
+
const dashboardsResponse = await this.axiosInstance.get('/api/dashboard');
|
|
172
|
+
this.logInfo('Successfully listed resources', { count: dashboardsResponse.data.length });
|
|
173
|
+
// 将仪表板作为资源返回
|
|
174
|
+
return {
|
|
175
|
+
resources: dashboardsResponse.data.map((dashboard) => ({
|
|
176
|
+
uri: `metabase://dashboard/${dashboard.id}`,
|
|
177
|
+
mimeType: "application/json",
|
|
178
|
+
name: dashboard.name,
|
|
179
|
+
description: `Metabase dashboard: ${dashboard.name}`
|
|
180
|
+
}))
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
this.logError('Failed to list resources', error);
|
|
185
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to list Metabase resources');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// 资源模板
|
|
189
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
190
|
+
return {
|
|
191
|
+
resourceTemplates: [
|
|
192
|
+
{
|
|
193
|
+
uriTemplate: 'metabase://dashboard/{id}',
|
|
194
|
+
name: 'Dashboard by ID',
|
|
195
|
+
mimeType: 'application/json',
|
|
196
|
+
description: 'Get a Metabase dashboard by its ID',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
uriTemplate: 'metabase://card/{id}',
|
|
200
|
+
name: 'Card by ID',
|
|
201
|
+
mimeType: 'application/json',
|
|
202
|
+
description: 'Get a Metabase question/card by its ID',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
uriTemplate: 'metabase://database/{id}',
|
|
206
|
+
name: 'Database by ID',
|
|
207
|
+
mimeType: 'application/json',
|
|
208
|
+
description: 'Get a Metabase database by its ID',
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
// 读取资源
|
|
214
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
215
|
+
this.logInfo('Reading resource...', { requestStructure: JSON.stringify(request) });
|
|
216
|
+
if (!METABASE_API_KEY) {
|
|
217
|
+
await this.getSessionToken();
|
|
218
|
+
}
|
|
219
|
+
const uri = request.params?.uri;
|
|
220
|
+
let match;
|
|
221
|
+
try {
|
|
222
|
+
// 处理仪表板资源
|
|
223
|
+
if ((match = uri.match(/^metabase:\/\/dashboard\/(\d+)$/))) {
|
|
224
|
+
const dashboardId = match[1];
|
|
225
|
+
const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
|
|
226
|
+
return {
|
|
227
|
+
contents: [{
|
|
228
|
+
uri: request.params?.uri,
|
|
229
|
+
mimeType: "application/json",
|
|
230
|
+
text: JSON.stringify(response.data, null, 2)
|
|
231
|
+
}]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// 处理问题/卡片资源
|
|
235
|
+
else if ((match = uri.match(/^metabase:\/\/card\/(\d+)$/))) {
|
|
236
|
+
const cardId = match[1];
|
|
237
|
+
const response = await this.axiosInstance.get(`/api/card/${cardId}`);
|
|
238
|
+
return {
|
|
239
|
+
contents: [{
|
|
240
|
+
uri: request.params?.uri,
|
|
241
|
+
mimeType: "application/json",
|
|
242
|
+
text: JSON.stringify(response.data, null, 2)
|
|
243
|
+
}]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// 处理数据库资源
|
|
247
|
+
else if ((match = uri.match(/^metabase:\/\/database\/(\d+)$/))) {
|
|
248
|
+
const databaseId = match[1];
|
|
249
|
+
const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
250
|
+
return {
|
|
251
|
+
contents: [{
|
|
252
|
+
uri: request.params?.uri,
|
|
253
|
+
mimeType: "application/json",
|
|
254
|
+
text: JSON.stringify(response.data, null, 2)
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid URI format: ${uri}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (axios.isAxiosError(error)) {
|
|
264
|
+
throw new McpError(ErrorCode.InternalError, `Metabase API error: ${error.response?.data?.message || error.message}`);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 设置工具处理程序
|
|
272
|
+
*/
|
|
273
|
+
setupToolHandlers() {
|
|
274
|
+
// No session token needed for listing tools, as it's static data
|
|
275
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
276
|
+
return {
|
|
277
|
+
tools: [
|
|
278
|
+
{
|
|
279
|
+
name: "list_dashboards",
|
|
280
|
+
description: "List all dashboards in Metabase",
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "list_cards",
|
|
288
|
+
description: "List all questions/cards in Metabase",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
f: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine"
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "list_databases",
|
|
301
|
+
description: "List all databases in Metabase",
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "execute_card",
|
|
309
|
+
description: "Execute a Metabase question/card and get results",
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
card_id: {
|
|
314
|
+
type: "number",
|
|
315
|
+
description: "ID of the card/question to execute"
|
|
316
|
+
},
|
|
317
|
+
parameters: {
|
|
318
|
+
type: "object",
|
|
319
|
+
description: "Optional parameters for the query"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
required: ["card_id"]
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "get_dashboard_cards",
|
|
327
|
+
description: "Get all cards in a dashboard",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: {
|
|
331
|
+
dashboard_id: {
|
|
332
|
+
type: "number",
|
|
333
|
+
description: "ID of the dashboard"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
required: ["dashboard_id"]
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "execute_query",
|
|
341
|
+
description: "Execute a SQL query against a Metabase database",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
database_id: {
|
|
346
|
+
type: "number",
|
|
347
|
+
description: "ID of the database to query"
|
|
348
|
+
},
|
|
349
|
+
query: {
|
|
350
|
+
type: "string",
|
|
351
|
+
description: "SQL query to execute"
|
|
352
|
+
},
|
|
353
|
+
native_parameters: {
|
|
354
|
+
type: "array",
|
|
355
|
+
description: "Optional parameters for the query",
|
|
356
|
+
items: {
|
|
357
|
+
type: "object"
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
required: ["database_id", "query"]
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "create_card",
|
|
366
|
+
description: "Create a new Metabase question (card).",
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
name: { type: "string", description: "Name of the card" },
|
|
371
|
+
dataset_query: { type: "object", description: "The query for the card (e.g., MBQL or native query)" },
|
|
372
|
+
display: { type: "string", description: "Display type (e.g., 'table', 'line', 'bar')" },
|
|
373
|
+
visualization_settings: { type: "object", description: "Settings for the visualization" },
|
|
374
|
+
collection_id: { type: "number", description: "Optional ID of the collection to save the card in" },
|
|
375
|
+
description: { type: "string", description: "Optional description for the card" }
|
|
376
|
+
},
|
|
377
|
+
required: ["name", "dataset_query", "display", "visualization_settings"]
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "update_card",
|
|
382
|
+
description: "Update an existing Metabase question (card).",
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
card_id: { type: "number", description: "ID of the card to update" },
|
|
387
|
+
name: { type: "string", description: "New name for the card" },
|
|
388
|
+
dataset_query: { type: "object", description: "New query for the card" },
|
|
389
|
+
display: { type: "string", description: "New display type" },
|
|
390
|
+
visualization_settings: { type: "object", description: "New visualization settings" },
|
|
391
|
+
collection_id: { type: "number", description: "New collection ID" },
|
|
392
|
+
description: { type: "string", description: "New description" },
|
|
393
|
+
archived: { type: "boolean", description: "Set to true to archive the card" }
|
|
394
|
+
},
|
|
395
|
+
required: ["card_id"]
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "delete_card",
|
|
400
|
+
description: "Delete a Metabase question (card).",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
card_id: { type: "number", description: "ID of the card to delete" },
|
|
405
|
+
hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false }
|
|
406
|
+
},
|
|
407
|
+
required: ["card_id"]
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "create_dashboard",
|
|
412
|
+
description: "Create a new Metabase dashboard.",
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
name: { type: "string", description: "Name of the dashboard" },
|
|
417
|
+
description: { type: "string", description: "Optional description for the dashboard" },
|
|
418
|
+
parameters: { type: "array", description: "Optional parameters for the dashboard", items: { type: "object" } },
|
|
419
|
+
collection_id: { type: "number", description: "Optional ID of the collection to save the dashboard in" }
|
|
420
|
+
},
|
|
421
|
+
required: ["name"]
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "update_dashboard",
|
|
426
|
+
description: "Update an existing Metabase dashboard.",
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
dashboard_id: { type: "number", description: "ID of the dashboard to update" },
|
|
431
|
+
name: { type: "string", description: "New name for the dashboard" },
|
|
432
|
+
description: { type: "string", description: "New description for the dashboard" },
|
|
433
|
+
parameters: { type: "array", description: "New parameters for the dashboard", items: { type: "object" } },
|
|
434
|
+
collection_id: { type: "number", description: "New collection ID" },
|
|
435
|
+
archived: { type: "boolean", description: "Set to true to archive the dashboard" }
|
|
436
|
+
},
|
|
437
|
+
required: ["dashboard_id"]
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: "delete_dashboard",
|
|
442
|
+
description: "Delete a Metabase dashboard.",
|
|
443
|
+
inputSchema: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
dashboard_id: { type: "number", description: "ID of the dashboard to delete" },
|
|
447
|
+
hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false }
|
|
448
|
+
},
|
|
449
|
+
required: ["dashboard_id"]
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
456
|
+
this.logInfo('Calling tool...', { requestStructure: JSON.stringify(request) });
|
|
457
|
+
if (!METABASE_API_KEY) {
|
|
458
|
+
await this.getSessionToken();
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
switch (request.params?.name) {
|
|
462
|
+
case "list_dashboards": {
|
|
463
|
+
const response = await this.axiosInstance.get('/api/dashboard');
|
|
464
|
+
return {
|
|
465
|
+
content: [{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: JSON.stringify(response.data, null, 2)
|
|
468
|
+
}]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
case "list_cards": {
|
|
472
|
+
const f = request.params?.arguments?.f || "all";
|
|
473
|
+
const response = await this.axiosInstance.get(`/api/card?f=${f}`);
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: JSON.stringify(response.data, null, 2)
|
|
478
|
+
}]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
case "list_databases": {
|
|
482
|
+
const response = await this.axiosInstance.get('/api/database');
|
|
483
|
+
return {
|
|
484
|
+
content: [{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: JSON.stringify(response.data, null, 2)
|
|
487
|
+
}]
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
case "execute_card": {
|
|
491
|
+
const cardId = request.params?.arguments?.card_id;
|
|
492
|
+
if (!cardId) {
|
|
493
|
+
throw new McpError(ErrorCode.InvalidParams, "Card ID is required");
|
|
494
|
+
}
|
|
495
|
+
const parameters = request.params?.arguments?.parameters || {};
|
|
496
|
+
const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters });
|
|
497
|
+
return {
|
|
498
|
+
content: [{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: JSON.stringify(response.data, null, 2)
|
|
501
|
+
}]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
case "get_dashboard_cards": {
|
|
505
|
+
const dashboardId = request.params?.arguments?.dashboard_id;
|
|
506
|
+
if (!dashboardId) {
|
|
507
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required");
|
|
508
|
+
}
|
|
509
|
+
const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
|
|
510
|
+
return {
|
|
511
|
+
content: [{
|
|
512
|
+
type: "text",
|
|
513
|
+
text: JSON.stringify(response.data.cards, null, 2)
|
|
514
|
+
}]
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
case "execute_query": {
|
|
518
|
+
const databaseId = request.params?.arguments?.database_id;
|
|
519
|
+
const query = request.params?.arguments?.query;
|
|
520
|
+
const nativeParameters = request.params?.arguments?.native_parameters || [];
|
|
521
|
+
if (!databaseId) {
|
|
522
|
+
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
523
|
+
}
|
|
524
|
+
if (!query) {
|
|
525
|
+
throw new McpError(ErrorCode.InvalidParams, "SQL query is required");
|
|
526
|
+
}
|
|
527
|
+
// 构建查询请求体
|
|
528
|
+
const queryData = {
|
|
529
|
+
type: "native",
|
|
530
|
+
native: {
|
|
531
|
+
query: query,
|
|
532
|
+
template_tags: {}
|
|
533
|
+
},
|
|
534
|
+
parameters: nativeParameters,
|
|
535
|
+
database: databaseId
|
|
536
|
+
};
|
|
537
|
+
const response = await this.axiosInstance.post('/api/dataset', queryData);
|
|
538
|
+
return {
|
|
539
|
+
content: [{
|
|
540
|
+
type: "text",
|
|
541
|
+
text: JSON.stringify(response.data, null, 2)
|
|
542
|
+
}]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
case "create_card": {
|
|
546
|
+
const { name, dataset_query, display, visualization_settings, collection_id, description } = request.params?.arguments || {};
|
|
547
|
+
if (!name || !dataset_query || !display || !visualization_settings) {
|
|
548
|
+
throw new McpError(ErrorCode.InvalidParams, "Missing required fields for create_card: name, dataset_query, display, visualization_settings");
|
|
549
|
+
}
|
|
550
|
+
const createCardBody = {
|
|
551
|
+
name,
|
|
552
|
+
dataset_query,
|
|
553
|
+
display,
|
|
554
|
+
visualization_settings,
|
|
555
|
+
};
|
|
556
|
+
if (collection_id !== undefined)
|
|
557
|
+
createCardBody.collection_id = collection_id;
|
|
558
|
+
if (description !== undefined)
|
|
559
|
+
createCardBody.description = description;
|
|
560
|
+
const response = await this.axiosInstance.post('/api/card', createCardBody);
|
|
561
|
+
return {
|
|
562
|
+
content: [{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: JSON.stringify(response.data, null, 2)
|
|
565
|
+
}]
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
case "update_card": {
|
|
569
|
+
const { card_id, ...updateFields } = request.params?.arguments || {};
|
|
570
|
+
if (!card_id) {
|
|
571
|
+
throw new McpError(ErrorCode.InvalidParams, "Card ID is required for update_card");
|
|
572
|
+
}
|
|
573
|
+
if (Object.keys(updateFields).length === 0) {
|
|
574
|
+
throw new McpError(ErrorCode.InvalidParams, "No fields provided for update_card");
|
|
575
|
+
}
|
|
576
|
+
const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields);
|
|
577
|
+
return {
|
|
578
|
+
content: [{
|
|
579
|
+
type: "text",
|
|
580
|
+
text: JSON.stringify(response.data, null, 2)
|
|
581
|
+
}]
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
case "delete_card": {
|
|
585
|
+
const { card_id, hard_delete = false } = request.params?.arguments || {};
|
|
586
|
+
if (!card_id) {
|
|
587
|
+
throw new McpError(ErrorCode.InvalidParams, "Card ID is required for delete_card");
|
|
588
|
+
}
|
|
589
|
+
if (hard_delete) {
|
|
590
|
+
await this.axiosInstance.delete(`/api/card/${card_id}`);
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: `Card ${card_id} permanently deleted.`
|
|
595
|
+
}]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// Soft delete (archive)
|
|
600
|
+
const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true });
|
|
601
|
+
return {
|
|
602
|
+
content: [{
|
|
603
|
+
type: "text",
|
|
604
|
+
// Metabase might return the updated card object or just a success status.
|
|
605
|
+
// If response.data is available and meaningful, include it. Otherwise, a generic success message.
|
|
606
|
+
text: response.data ? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}` : `Card ${card_id} archived.`
|
|
607
|
+
}]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
case "create_dashboard": {
|
|
612
|
+
const { name, description, parameters, collection_id } = request.params?.arguments || {};
|
|
613
|
+
if (!name) {
|
|
614
|
+
throw new McpError(ErrorCode.InvalidParams, "Missing required field for create_dashboard: name");
|
|
615
|
+
}
|
|
616
|
+
const createDashboardBody = { name };
|
|
617
|
+
if (description !== undefined)
|
|
618
|
+
createDashboardBody.description = description;
|
|
619
|
+
if (parameters !== undefined)
|
|
620
|
+
createDashboardBody.parameters = parameters;
|
|
621
|
+
if (collection_id !== undefined)
|
|
622
|
+
createDashboardBody.collection_id = collection_id;
|
|
623
|
+
const response = await this.axiosInstance.post('/api/dashboard', createDashboardBody);
|
|
624
|
+
return {
|
|
625
|
+
content: [{
|
|
626
|
+
type: "text",
|
|
627
|
+
text: JSON.stringify(response.data, null, 2)
|
|
628
|
+
}]
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
case "update_dashboard": {
|
|
632
|
+
const { dashboard_id, ...updateFields } = request.params?.arguments || {};
|
|
633
|
+
if (!dashboard_id) {
|
|
634
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for update_dashboard");
|
|
635
|
+
}
|
|
636
|
+
if (Object.keys(updateFields).length === 0) {
|
|
637
|
+
throw new McpError(ErrorCode.InvalidParams, "No fields provided for update_dashboard");
|
|
638
|
+
}
|
|
639
|
+
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields);
|
|
640
|
+
return {
|
|
641
|
+
content: [{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: JSON.stringify(response.data, null, 2)
|
|
644
|
+
}]
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
case "delete_dashboard": {
|
|
648
|
+
const { dashboard_id, hard_delete = false } = request.params?.arguments || {};
|
|
649
|
+
if (!dashboard_id) {
|
|
650
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for delete_dashboard");
|
|
651
|
+
}
|
|
652
|
+
if (hard_delete) {
|
|
653
|
+
await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`);
|
|
654
|
+
return {
|
|
655
|
+
content: [{
|
|
656
|
+
type: "text",
|
|
657
|
+
text: `Dashboard ${dashboard_id} permanently deleted.`
|
|
658
|
+
}]
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
// Soft delete (archive)
|
|
663
|
+
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true });
|
|
664
|
+
return {
|
|
665
|
+
content: [{
|
|
666
|
+
type: "text",
|
|
667
|
+
text: response.data ? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}` : `Dashboard ${dashboard_id} archived.`
|
|
668
|
+
}]
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
default:
|
|
673
|
+
return {
|
|
674
|
+
content: [
|
|
675
|
+
{
|
|
676
|
+
type: "text",
|
|
677
|
+
text: `Unknown tool: ${request.params?.name}`
|
|
678
|
+
}
|
|
679
|
+
],
|
|
680
|
+
isError: true
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (axios.isAxiosError(error)) {
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: `Metabase API error: ${error.response?.data?.message || error.message}`
|
|
690
|
+
}],
|
|
691
|
+
isError: true
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
async run() {
|
|
699
|
+
try {
|
|
700
|
+
this.logInfo('Starting Metabase MCP server...');
|
|
701
|
+
const transport = new StdioServerTransport();
|
|
702
|
+
await this.server.connect(transport);
|
|
703
|
+
this.logInfo('Metabase MCP server running on stdio');
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
this.logError('Failed to start server', error);
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Add global error handlers
|
|
712
|
+
process.on('uncaughtException', (error) => {
|
|
713
|
+
console.error(JSON.stringify({
|
|
714
|
+
timestamp: new Date().toISOString(),
|
|
715
|
+
level: 'fatal',
|
|
716
|
+
message: 'Uncaught Exception',
|
|
717
|
+
error: error.message,
|
|
718
|
+
stack: error.stack
|
|
719
|
+
}));
|
|
720
|
+
process.exit(1);
|
|
721
|
+
});
|
|
722
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
723
|
+
const errorMessage = reason instanceof Error ? reason.message : String(reason);
|
|
724
|
+
console.error(JSON.stringify({
|
|
725
|
+
timestamp: new Date().toISOString(),
|
|
726
|
+
level: 'fatal',
|
|
727
|
+
message: 'Unhandled Rejection',
|
|
728
|
+
error: errorMessage
|
|
729
|
+
}));
|
|
730
|
+
});
|
|
731
|
+
const server = new MetabaseServer();
|
|
732
|
+
server.run().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blixify-charts-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Blixify Charts MCP",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"metabase-server": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\" && mkdir -p dist && cp build/index.js dist/index.js",
|
|
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.1",
|
|
21
|
+
"abort-controller": "^3.0.0",
|
|
22
|
+
"axios": "^1.8.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/axios": "^0.14.4",
|
|
26
|
+
"@types/node": "^20.17.22",
|
|
27
|
+
"typescript": "^5.3.3"
|
|
28
|
+
}
|
|
29
|
+
}
|