justoneapi-mcp 1.0.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/LICENSE +21 -0
- package/README.md +420 -0
- package/README.zh-CN.md +429 -0
- package/dist/common/config.js +44 -0
- package/dist/common/errors.js +86 -0
- package/dist/common/http.js +82 -0
- package/dist/index.js +76 -0
- package/dist/tools/kuaishou/search_video_v2.js +13 -0
- package/dist/tools/search/unified_search_v1.js +58 -0
- package/dist/tools/unified_search_v1.js +58 -0
- package/dist/version.js +15 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JustOneAPI MCP Contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/justoneapi-mcp)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
|
|
4
|
+
[简体中文](README.zh-CN.md) | English
|
|
5
|
+
|
|
6
|
+
> **Raw JSON by design.** Unmodified upstream responses for maximum data fidelity.
|
|
7
|
+
|
|
8
|
+
# JustOneAPI MCP Server
|
|
9
|
+
|
|
10
|
+
Use JustOneAPI inside AI assistants via Model Context Protocol (MCP).
|
|
11
|
+
|
|
12
|
+
This MCP server is a thin transport layer that exposes JustOneAPI endpoints to AI agents and returns the original raw JSON response from upstream platforms.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What This MCP Does
|
|
17
|
+
|
|
18
|
+
- Exposes JustOneAPI endpoints as MCP tools
|
|
19
|
+
- Handles authentication, retries, timeouts, and error normalization
|
|
20
|
+
- Returns raw upstream JSON without field parsing
|
|
21
|
+
- Designed for AI agents and developer workflows
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What This MCP Does NOT Do
|
|
26
|
+
|
|
27
|
+
- No field parsing or schema normalization
|
|
28
|
+
- No data restructuring
|
|
29
|
+
- No assumptions about upstream response structure
|
|
30
|
+
|
|
31
|
+
This design is intentional to preserve data fidelity and long-term compatibility.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Available Tools
|
|
36
|
+
|
|
37
|
+
This MCP server provides multiple tools to interact with JustOneAPI endpoints. Each tool returns the original raw JSON response from upstream without field parsing.
|
|
38
|
+
|
|
39
|
+
### Featured Tool: Unified Search
|
|
40
|
+
|
|
41
|
+
**`unified_search_v1`** - Search across multiple Chinese social media and news platforms in one request.
|
|
42
|
+
|
|
43
|
+
Supports: Weibo, WeChat, Zhihu, Douyin, Xiaohongshu, Bilibili, Kuaishou, and News.
|
|
44
|
+
|
|
45
|
+
**How to use (Natural Language):**
|
|
46
|
+
|
|
47
|
+
Just ask Claude or your MCP host to use the tool with a natural language request:
|
|
48
|
+
- *"Search for AI discussions on Chinese social media from last week"*
|
|
49
|
+
- *"Find posts about deepseek on Weibo from January 1st to 5th"*
|
|
50
|
+
- *"Search for chatgpt OR 机器学习 on all platforms, exclude 广告"*
|
|
51
|
+
|
|
52
|
+
Claude will automatically:
|
|
53
|
+
- Convert your request to the correct API format
|
|
54
|
+
- Handle date formatting (UTC+8 timezone)
|
|
55
|
+
- Process pagination with nextCursor (just ask "show me more")
|
|
56
|
+
- Return and summarize results
|
|
57
|
+
|
|
58
|
+
**Search Syntax:**
|
|
59
|
+
- Single keyword: `deepseek`
|
|
60
|
+
- AND search: `deepseek chatgpt` (both must appear)
|
|
61
|
+
- OR search: `deepseek~chatgpt` (either can appear)
|
|
62
|
+
- NOT search: `deepseek -chatgpt` (exclude chatgpt)
|
|
63
|
+
|
|
64
|
+
**Platform Options:**
|
|
65
|
+
`ALL` (default), `NEWS`, `WEIBO`, `WEIXIN`, `ZHIHU`, `DOUYIN`, `XIAOHONGSHU`, `BILIBILI`, `KUAISHOU`
|
|
66
|
+
|
|
67
|
+
**Technical Parameters (for reference):**
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"keyword": "AI",
|
|
71
|
+
"source": "ALL",
|
|
72
|
+
"start": "2025-01-01 00:00:00",
|
|
73
|
+
"end": "2025-01-02 23:59:59"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Discovering All Available Tools
|
|
78
|
+
|
|
79
|
+
To see all available tools in your MCP host (Claude Desktop, Cursor, etc.):
|
|
80
|
+
|
|
81
|
+
**In Claude Desktop:**
|
|
82
|
+
```
|
|
83
|
+
Please list all available tools from justoneapi-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**In Cursor or other MCP hosts:**
|
|
87
|
+
Use your host's tool discovery feature to see all available tools and their parameters.
|
|
88
|
+
|
|
89
|
+
Each tool includes:
|
|
90
|
+
- ✅ Complete parameter descriptions
|
|
91
|
+
- ✅ Input validation with Zod schemas
|
|
92
|
+
- ✅ Detailed error messages
|
|
93
|
+
- ✅ Example values in parameter descriptions
|
|
94
|
+
|
|
95
|
+
### Tool Naming Convention
|
|
96
|
+
|
|
97
|
+
All tools follow this pattern:
|
|
98
|
+
- `unified_search_v1` - Unified search across platforms
|
|
99
|
+
- `kuaishou_search_video_v2` - Platform-specific video search
|
|
100
|
+
- `{platform}_{action}_{version}` - General pattern
|
|
101
|
+
|
|
102
|
+
### Complete Tool Documentation
|
|
103
|
+
|
|
104
|
+
For detailed documentation of all tools, parameters, and examples, see **[TOOLS.md](TOOLS.md)**.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Output Contract
|
|
109
|
+
|
|
110
|
+
This MCP returns raw JSON from upstream APIs.
|
|
111
|
+
|
|
112
|
+
Example (simplified):
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
"code": 0,
|
|
116
|
+
"message": null,
|
|
117
|
+
"recordTime": "2025-12-31T14:55:21Z",
|
|
118
|
+
"data": ...
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Authentication
|
|
124
|
+
|
|
125
|
+
You need a JustOneAPI token to use this server.
|
|
126
|
+
|
|
127
|
+
**Get your token**: Visit [https://justoneapi.com](https://justoneapi.com) to sign up and obtain your API token.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Installation
|
|
132
|
+
|
|
133
|
+
### Option 1: npx (Recommended)
|
|
134
|
+
|
|
135
|
+
Use directly with npx, no installation required.
|
|
136
|
+
|
|
137
|
+
**Configure Claude Desktop:**
|
|
138
|
+
|
|
139
|
+
Edit the configuration file for your operating system:
|
|
140
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
141
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
142
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"justoneapi": {
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
150
|
+
"env": {
|
|
151
|
+
"JUSTONEAPI_TOKEN": "your_actual_token_here"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> 💡 **Get your token**: Sign up at [justoneapi.com](https://justoneapi.com)
|
|
159
|
+
|
|
160
|
+
### Option 2: Global Installation
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm install -g justoneapi-mcp
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Configure Claude Desktop:**
|
|
167
|
+
|
|
168
|
+
Edit the configuration file for your operating system:
|
|
169
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
170
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
171
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"justoneapi": {
|
|
177
|
+
"command": "justoneapi-mcp",
|
|
178
|
+
"env": {
|
|
179
|
+
"JUSTONEAPI_TOKEN": "your_actual_token_here"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
> 💡 **Get your token**: Sign up at [justoneapi.com](https://justoneapi.com)
|
|
187
|
+
|
|
188
|
+
### Option 3: Local Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
git clone <repository>
|
|
192
|
+
cd justoneapi-mcp
|
|
193
|
+
npm install
|
|
194
|
+
npm run build
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Configure Claude Desktop:**
|
|
198
|
+
|
|
199
|
+
Edit the configuration file for your operating system:
|
|
200
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
201
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
202
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"mcpServers": {
|
|
207
|
+
"justoneapi": {
|
|
208
|
+
"command": "node",
|
|
209
|
+
"args": ["/absolute/path/to/justoneapi-mcp/dist/index.js"],
|
|
210
|
+
"env": {
|
|
211
|
+
"JUSTONEAPI_TOKEN": "your_actual_token_here"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> 💡 **Get your token**: Sign up at [justoneapi.com](https://justoneapi.com)
|
|
219
|
+
|
|
220
|
+
That's it! Only the token is required.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Usage with Other MCP Hosts
|
|
225
|
+
|
|
226
|
+
This server follows the standard MCP protocol and is compatible with any MCP host (Cursor, Cline, custom agents, etc.).
|
|
227
|
+
|
|
228
|
+
Generic configuration:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"command": "npx",
|
|
233
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
234
|
+
"env": {
|
|
235
|
+
"JUSTONEAPI_TOKEN": "your_actual_token_here"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Or if globally installed:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"command": "justoneapi-mcp",
|
|
245
|
+
"env": {
|
|
246
|
+
"JUSTONEAPI_TOKEN": "your_actual_token_here"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> 💡 **Get your token**: Sign up at [justoneapi.com](https://justoneapi.com)
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Quick Start
|
|
256
|
+
|
|
257
|
+
### 1) Restart Your MCP Host
|
|
258
|
+
|
|
259
|
+
After configuring, restart Claude Desktop (Cmd + Q) or your MCP host.
|
|
260
|
+
|
|
261
|
+
### 2) Test the Connection
|
|
262
|
+
|
|
263
|
+
In Claude Desktop, ask:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
Please list all available tools from justoneapi-mcp
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
You should see the available tools including `unified_search_v1` and `kuaishou_search_video_v2`.
|
|
270
|
+
|
|
271
|
+
### 3) Use the Unified Search
|
|
272
|
+
|
|
273
|
+
Try searching across multiple platforms:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
Use the unified_search_v1 tool to search for "AI" across all platforms
|
|
277
|
+
from January 1st to January 2nd, 2025.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Claude will automatically format the request and return results from Weibo, WeChat, Zhihu, Douyin, Xiaohongshu, Bilibili, Kuaishou, and News.
|
|
281
|
+
|
|
282
|
+
**Example conversation:**
|
|
283
|
+
|
|
284
|
+
**You:** Search for recent discussions about "deepseek" on Chinese social media platforms from the last week.
|
|
285
|
+
|
|
286
|
+
**Claude:** I'll search for "deepseek" across multiple platforms using the unified search tool.
|
|
287
|
+
|
|
288
|
+
*[Claude uses unified_search_v1 with appropriate date range and returns aggregated results]*
|
|
289
|
+
|
|
290
|
+
### 4) Advanced Search Examples
|
|
291
|
+
|
|
292
|
+
**Platform-specific search:**
|
|
293
|
+
```
|
|
294
|
+
Search for "chatgpt" on Weibo only, from December 1st to January 2nd
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Complex search queries:**
|
|
298
|
+
```
|
|
299
|
+
Search for posts containing "AI" OR "机器学习" but NOT "广告" on Zhihu,
|
|
300
|
+
from the last 30 days
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Pagination (Getting more results):**
|
|
304
|
+
|
|
305
|
+
When search results have more pages, the response includes a `nextCursor` field. Simply ask Claude to continue:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Show me the next page of results
|
|
309
|
+
```
|
|
310
|
+
or
|
|
311
|
+
```
|
|
312
|
+
Get more results from the previous search
|
|
313
|
+
```
|
|
314
|
+
or
|
|
315
|
+
```
|
|
316
|
+
Continue with the next page
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Claude will automatically:
|
|
320
|
+
- Extract the `nextCursor` from the previous response
|
|
321
|
+
- Use it to fetch the next page
|
|
322
|
+
- Continue until no more results are available
|
|
323
|
+
|
|
324
|
+
**Example conversation with pagination:**
|
|
325
|
+
|
|
326
|
+
**You:** Search for "AI" on all platforms from January 1-5, 2025
|
|
327
|
+
|
|
328
|
+
**Claude:** *[Returns first page of results with 10-20 items]*
|
|
329
|
+
|
|
330
|
+
**You:** Show me more results
|
|
331
|
+
|
|
332
|
+
**Claude:** *[Uses nextCursor to fetch page 2]*
|
|
333
|
+
|
|
334
|
+
**You:** Continue
|
|
335
|
+
|
|
336
|
+
**Claude:** *[Fetches page 3, and so on...]*
|
|
337
|
+
|
|
338
|
+
💡 **Note**: When using `nextCursor` for pagination, you don't need to provide `start`, `end`, or `source` again - the cursor already contains this information.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Error Handling
|
|
343
|
+
|
|
344
|
+
All errors are normalized into stable MCP error codes with actionable messages.
|
|
345
|
+
|
|
346
|
+
### Error Codes
|
|
347
|
+
|
|
348
|
+
| Error Code | Description | Action |
|
|
349
|
+
|------------|-------------|-------------------------------------|
|
|
350
|
+
| `INVALID_TOKEN` | Token is invalid or inactive | Update your `JUSTONEAPI_TOKEN` |
|
|
351
|
+
| `COLLECT_FAILED` | Data collection failed | Retry after a short delay |
|
|
352
|
+
| `RATE_LIMITED` | Too many requests | Slow down and retry later |
|
|
353
|
+
| `DAILY_QUOTA_EXCEEDED` | Daily usage limit reached | Wait until tomorrow or upgrade plan |
|
|
354
|
+
| `INSUFFICIENT_BALANCE` | Account balance too low | Top up your account |
|
|
355
|
+
| `PERMISSION_DENIED` | No access to this resource | Contact support |
|
|
356
|
+
| `VALIDATION_ERROR` | Invalid request parameters | Check input values |
|
|
357
|
+
| `INTERNAL_ERROR` | Server error | Retry later |
|
|
358
|
+
| `NETWORK_TIMEOUT` | Request timed out | Check network or retry |
|
|
359
|
+
| `NETWORK_ERROR` | Network connection failed | Check internet connection |
|
|
360
|
+
| `UPSTREAM_ERROR` | Unspecified upstream error | Retry or contact support |
|
|
361
|
+
|
|
362
|
+
### Error Format
|
|
363
|
+
|
|
364
|
+
Errors are returned in this format:
|
|
365
|
+
```
|
|
366
|
+
ERROR[ERROR_CODE] (upstream=XXX): Human-readable message
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
```
|
|
371
|
+
ERROR[RATE_LIMITED] (upstream=302): Rate limit exceeded. Please slow down and retry later.
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Design Philosophy
|
|
377
|
+
|
|
378
|
+
Transport, not transformation.
|
|
379
|
+
|
|
380
|
+
This MCP prioritizes stability, transparency, and raw data fidelity over convenience.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Advanced Configuration (Optional)
|
|
385
|
+
|
|
386
|
+
By default, the server works with sensible defaults. You only need to set `JUSTONEAPI_TOKEN`.
|
|
387
|
+
|
|
388
|
+
However, you can customize behavior with these optional environment variables:
|
|
389
|
+
|
|
390
|
+
| Variable | Default | Description |
|
|
391
|
+
|----------|---------|-------------|
|
|
392
|
+
| `JUSTONEAPI_TOKEN` | *(required)* | Your JustOneAPI token |
|
|
393
|
+
| `JUSTONEAPI_BASE_URL` | `https://api.justoneapi.com` | API endpoint |
|
|
394
|
+
| `JUSTONEAPI_TIMEOUT_MS` | `20000` | Request timeout (milliseconds) |
|
|
395
|
+
| `JUSTONEAPI_RETRY` | `1` | Number of retries after first attempt |
|
|
396
|
+
| `JUSTONEAPI_DEBUG` | `false` | Enable debug logging to stderr |
|
|
397
|
+
|
|
398
|
+
Example with custom settings:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"mcpServers": {
|
|
403
|
+
"justoneapi": {
|
|
404
|
+
"command": "npx",
|
|
405
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
406
|
+
"env": {
|
|
407
|
+
"JUSTONEAPI_TOKEN": "your_token_here",
|
|
408
|
+
"JUSTONEAPI_TIMEOUT_MS": "30000",
|
|
409
|
+
"JUSTONEAPI_DEBUG": "true"
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/justoneapi-mcp)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
|
|
4
|
+
[English](README.md) | 简体中文
|
|
5
|
+
|
|
6
|
+
> **原始 JSON 设计。** 返回未经修改的上游响应,确保最大数据保真度。
|
|
7
|
+
|
|
8
|
+
# JustOneAPI MCP 服务器
|
|
9
|
+
|
|
10
|
+
在 AI 助手中通过模型上下文协议 (MCP) 使用 JustOneAPI。
|
|
11
|
+
|
|
12
|
+
这是一个轻量级的 MCP 服务器,将 JustOneAPI 接口暴露给 AI 代理,并返回来自上游平台的原始 JSON 响应。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 功能特性
|
|
17
|
+
|
|
18
|
+
- 将 JustOneAPI 接口暴露为 MCP 工具
|
|
19
|
+
- 处理认证、重试、超时和错误标准化
|
|
20
|
+
- 返回原始的上游 JSON,不解析字段
|
|
21
|
+
- 专为 AI 代理和开发者工作流设计
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 不做什么
|
|
26
|
+
|
|
27
|
+
- 不解析或标准化字段
|
|
28
|
+
- 不重构数据结构
|
|
29
|
+
- 不假设上游响应结构
|
|
30
|
+
|
|
31
|
+
这样的设计是有意为之,以保持数据保真度和长期兼容性。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 可用工具
|
|
36
|
+
|
|
37
|
+
本 MCP 服务器提供多个工具来与 JustOneAPI 接口交互。每个工具都返回未经解析的原始上游 JSON 响应。
|
|
38
|
+
|
|
39
|
+
### 特色工具:统一搜索
|
|
40
|
+
|
|
41
|
+
**`unified_search_v1`** - 一次请求搜索多个中文社交媒体和新闻平台。
|
|
42
|
+
|
|
43
|
+
支持平台:微博、微信、知乎、抖音、小红书、哔哩哔哩、快手和新闻。
|
|
44
|
+
|
|
45
|
+
**使用方法(自然语言):**
|
|
46
|
+
|
|
47
|
+
直接用自然语言向 Claude 或你的 MCP 客户端提问:
|
|
48
|
+
- *"搜索最近一周中文社交媒体上关于 AI 的讨论"*
|
|
49
|
+
- *"搜索微博上 1 月 1 日到 5 日关于 deepseek 的帖子"*
|
|
50
|
+
- *"在所有平台搜索 chatgpt 或 机器学习,排除 广告"*
|
|
51
|
+
|
|
52
|
+
Claude 会自动:
|
|
53
|
+
- 将你的请求转换为正确的 API 格式
|
|
54
|
+
- 处理日期格式化(UTC+8 时区)
|
|
55
|
+
- 使用 nextCursor 处理翻页(只需说"显示更多")
|
|
56
|
+
- 返回并总结结果
|
|
57
|
+
|
|
58
|
+
**搜索语法:**
|
|
59
|
+
- 单关键词:`deepseek`
|
|
60
|
+
- AND 搜索:`deepseek chatgpt`(两个词都必须出现)
|
|
61
|
+
- OR 搜索:`deepseek~chatgpt`(任一词出现即可)
|
|
62
|
+
- NOT 搜索:`deepseek -chatgpt`(排除 chatgpt)
|
|
63
|
+
|
|
64
|
+
**平台选项:**
|
|
65
|
+
`ALL`(默认,全部平台)、`NEWS`(新闻)、`WEIBO`(微博)、`WEIXIN`(微信)、`ZHIHU`(知乎)、`DOUYIN`(抖音)、`XIAOHONGSHU`(小红书)、`BILIBILI`(B站)、`KUAISHOU`(快手)
|
|
66
|
+
|
|
67
|
+
**技术参数(供参考):**
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"keyword": "AI",
|
|
71
|
+
"source": "ALL",
|
|
72
|
+
"start": "2025-01-01 00:00:00",
|
|
73
|
+
"end": "2025-01-02 23:59:59"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 发现所有可用工具
|
|
78
|
+
|
|
79
|
+
在你的 MCP 客户端(Claude Desktop、Cursor 等)中查看所有可用工具:
|
|
80
|
+
|
|
81
|
+
**在 Claude Desktop 中:**
|
|
82
|
+
```
|
|
83
|
+
请列出 justoneapi-mcp 的所有可用工具
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**在 Cursor 或其他 MCP 客户端:**
|
|
87
|
+
使用客户端的工具发现功能查看所有可用工具及其参数。
|
|
88
|
+
|
|
89
|
+
每个工具都包含:
|
|
90
|
+
- ✅ 完整的参数说明
|
|
91
|
+
- ✅ 使用 Zod 进行输入验证
|
|
92
|
+
- ✅ 详细的错误消息
|
|
93
|
+
- ✅ 参数说明中的示例值
|
|
94
|
+
|
|
95
|
+
### 工具命名规范
|
|
96
|
+
|
|
97
|
+
所有工具遵循此模式:
|
|
98
|
+
- `unified_search_v1` - 跨平台统一搜索
|
|
99
|
+
- `kuaishou_search_video_v2` - 平台特定的视频搜索
|
|
100
|
+
- `{平台}_{操作}_{版本}` - 通用模式
|
|
101
|
+
|
|
102
|
+
### 完整工具文档
|
|
103
|
+
|
|
104
|
+
详细的工具文档、参数和示例请参见 **[TOOLS.md](TOOLS.md)**。
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 输出约定
|
|
109
|
+
|
|
110
|
+
此 MCP 返回来自上游 API 的原始 JSON。
|
|
111
|
+
|
|
112
|
+
示例(简化):
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"code": 0,
|
|
117
|
+
"message": null,
|
|
118
|
+
"recordTime": "2025-12-31T14:55:21Z",
|
|
119
|
+
"data": ...
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 认证
|
|
126
|
+
|
|
127
|
+
你需要一个 JustOneAPI token 才能使用此服务器。
|
|
128
|
+
|
|
129
|
+
**获取你的 token**:访问 [https://justoneapi.com](https://justoneapi.com) 注册并获取 API token。
|
|
130
|
+
|
|
131
|
+
获得 token 后,在 MCP 客户端的环境变量中配置:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
JUSTONEAPI_TOKEN=你的实际token
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
🔒 **安全性**:token 作为查询参数传递给上游 API,绝不会以明文形式记录。所有 URL 在记录前都会脱敏处理。
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 安装
|
|
142
|
+
|
|
143
|
+
### 方式 1:npx(推荐)
|
|
144
|
+
|
|
145
|
+
直接使用 npx,无需安装。
|
|
146
|
+
|
|
147
|
+
**配置 Claude Desktop:**
|
|
148
|
+
|
|
149
|
+
编辑你的操作系统对应的配置文件:
|
|
150
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
151
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
152
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"justoneapi": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
160
|
+
"env": {
|
|
161
|
+
"JUSTONEAPI_TOKEN": "你的实际token"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> 💡 **获取你的 token**:在 [justoneapi.com](https://justoneapi.com) 注册
|
|
169
|
+
|
|
170
|
+
### 方式 2:全局安装
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm install -g justoneapi-mcp
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**配置 Claude Desktop:**
|
|
177
|
+
|
|
178
|
+
编辑你的操作系统对应的配置文件:
|
|
179
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
180
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
181
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"mcpServers": {
|
|
186
|
+
"justoneapi": {
|
|
187
|
+
"command": "justoneapi-mcp",
|
|
188
|
+
"env": {
|
|
189
|
+
"JUSTONEAPI_TOKEN": "你的实际token"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> 💡 **获取你的 token**:在 [justoneapi.com](https://justoneapi.com) 注册
|
|
197
|
+
|
|
198
|
+
### 方式 3:本地开发
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git clone <repository>
|
|
202
|
+
cd justoneapi-mcp
|
|
203
|
+
npm install
|
|
204
|
+
npm run build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**配置 Claude Desktop:**
|
|
208
|
+
|
|
209
|
+
编辑你的操作系统对应的配置文件:
|
|
210
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
211
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
212
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"mcpServers": {
|
|
217
|
+
"justoneapi": {
|
|
218
|
+
"command": "node",
|
|
219
|
+
"args": ["/绝对路径/justoneapi-mcp/dist/index.js"],
|
|
220
|
+
"env": {
|
|
221
|
+
"JUSTONEAPI_TOKEN": "你的实际token"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> 💡 **获取你的 token**:在 [justoneapi.com](https://justoneapi.com) 注册
|
|
229
|
+
|
|
230
|
+
就这么简单!只需要配置 token。
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 在其他 MCP 客户端中使用
|
|
235
|
+
|
|
236
|
+
此服务器遵循标准 MCP 协议,与任何 MCP 客户端兼容(Cursor、Cline、自定义代理等)。
|
|
237
|
+
|
|
238
|
+
通用配置:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"command": "npx",
|
|
243
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
244
|
+
"env": {
|
|
245
|
+
"JUSTONEAPI_TOKEN": "你的实际token"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
或如果已全局安装:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"command": "justoneapi-mcp",
|
|
255
|
+
"env": {
|
|
256
|
+
"JUSTONEAPI_TOKEN": "你的实际token"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
> 💡 **获取你的 token**:在 [justoneapi.com](https://justoneapi.com) 注册
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 快速开始
|
|
266
|
+
|
|
267
|
+
### 1) 重启 MCP 客户端
|
|
268
|
+
|
|
269
|
+
配置完成后,重启 Claude Desktop(Cmd + Q)或你的 MCP 客户端。
|
|
270
|
+
|
|
271
|
+
### 2) 测试连接
|
|
272
|
+
|
|
273
|
+
在 Claude Desktop 中提问:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
请列出 justoneapi-mcp 的所有可用工具
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
你应该能看到可用工具,包括 `unified_search_v1` 和 `kuaishou_search_video_v2`。
|
|
280
|
+
|
|
281
|
+
### 3) 使用统一搜索
|
|
282
|
+
|
|
283
|
+
尝试跨平台搜索:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
使用 unified_search_v1 工具搜索 2025 年 1 月 1 日到 2 日所有平台上关于 "AI" 的内容
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Claude 会自动格式化请求,并返回来自微博、微信、知乎、抖音、小红书、B站、快手和新闻的结果。
|
|
290
|
+
|
|
291
|
+
**示例对话:**
|
|
292
|
+
|
|
293
|
+
**你:** 搜索最近一周中文社交媒体平台上关于 "deepseek" 的讨论
|
|
294
|
+
|
|
295
|
+
**Claude:** 我将使用统一搜索工具搜索 "deepseek"。
|
|
296
|
+
|
|
297
|
+
*[Claude 使用 unified_search_v1 并适当的日期范围返回聚合结果]*
|
|
298
|
+
|
|
299
|
+
### 4) 高级搜索示例
|
|
300
|
+
|
|
301
|
+
**平台特定搜索:**
|
|
302
|
+
```
|
|
303
|
+
搜索 12 月 1 日到 1 月 2 日期间微博上关于 "chatgpt" 的内容
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**复杂搜索查询:**
|
|
307
|
+
```
|
|
308
|
+
在知乎搜索包含 "AI" 或 "机器学习" 但不包含 "广告" 的帖子,
|
|
309
|
+
时间范围为最近 30 天
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**翻页(获取更多结果):**
|
|
313
|
+
|
|
314
|
+
当搜索结果有更多页时,响应中会包含 `nextCursor` 字段。只需让 Claude 继续:
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
显示下一页结果
|
|
318
|
+
```
|
|
319
|
+
或
|
|
320
|
+
```
|
|
321
|
+
获取更多结果
|
|
322
|
+
```
|
|
323
|
+
或
|
|
324
|
+
```
|
|
325
|
+
继续下一页
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Claude 会自动:
|
|
329
|
+
- 从上一个响应中提取 `nextCursor`
|
|
330
|
+
- 使用它获取下一页
|
|
331
|
+
- 持续直到没有更多结果
|
|
332
|
+
|
|
333
|
+
**翻页对话示例:**
|
|
334
|
+
|
|
335
|
+
**你:** 搜索 2025 年 1 月 1-5 日所有平台上关于 "AI" 的内容
|
|
336
|
+
|
|
337
|
+
**Claude:** *[返回第 1 页结果,包含 10-20 条]*
|
|
338
|
+
|
|
339
|
+
**你:** 显示更多结果
|
|
340
|
+
|
|
341
|
+
**Claude:** *[使用 nextCursor 获取第 2 页]*
|
|
342
|
+
|
|
343
|
+
**你:** 继续
|
|
344
|
+
|
|
345
|
+
**Claude:** *[获取第 3 页,依此类推...]*
|
|
346
|
+
|
|
347
|
+
💡 **注意**:使用 `nextCursor` 翻页时,不需要再次提供 `start`、`end` 或 `source` - 游标已包含这些信息。
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 错误处理
|
|
352
|
+
|
|
353
|
+
所有错误都标准化为稳定的 MCP 错误代码,并附带可操作的消息。
|
|
354
|
+
|
|
355
|
+
### 错误代码
|
|
356
|
+
|
|
357
|
+
| 错误代码 | 说明 | 解决方案 |
|
|
358
|
+
|---------|------|---------|
|
|
359
|
+
| `INVALID_TOKEN` | Token 无效或未激活 | 更新你的 `JUSTONEAPI_TOKEN` |
|
|
360
|
+
| `COLLECT_FAILED` | 数据采集失败 | 稍后重试 |
|
|
361
|
+
| `RATE_LIMITED` | 请求过多 | 降低请求频率,稍后重试 |
|
|
362
|
+
| `DAILY_QUOTA_EXCEEDED` | 达到每日使用限额 | 等到明天或升级套餐 |
|
|
363
|
+
| `INSUFFICIENT_BALANCE` | 账户余额不足 | 充值你的账户 |
|
|
364
|
+
| `PERMISSION_DENIED` | 无权访问此资源 | 验证账户权限 |
|
|
365
|
+
| `VALIDATION_ERROR` | 请求参数无效 | 检查输入值 |
|
|
366
|
+
| `INTERNAL_ERROR` | 服务器错误 | 稍后重试 |
|
|
367
|
+
| `NETWORK_TIMEOUT` | 请求超时 | 检查网络或重试 |
|
|
368
|
+
| `NETWORK_ERROR` | 网络连接失败 | 检查互联网连接 |
|
|
369
|
+
| `UPSTREAM_ERROR` | 未指定的上游错误 | 重试或联系支持 |
|
|
370
|
+
|
|
371
|
+
### 错误格式
|
|
372
|
+
|
|
373
|
+
错误以此格式返回:
|
|
374
|
+
```
|
|
375
|
+
ERROR[错误代码] (upstream=XXX): 人类可读的消息
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
示例:
|
|
379
|
+
```
|
|
380
|
+
ERROR[RATE_LIMITED] (upstream=302): 超出速率限制。请降低速度并稍后重试。
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 设计理念
|
|
386
|
+
|
|
387
|
+
传输,而非转换。
|
|
388
|
+
|
|
389
|
+
此 MCP 优先考虑稳定性、透明度和原始数据保真度,而非便利性。
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 高级配置(可选)
|
|
394
|
+
|
|
395
|
+
默认情况下,服务器使用合理的默认值。你只需要设置 `JUSTONEAPI_TOKEN`。
|
|
396
|
+
|
|
397
|
+
但是,你可以使用这些可选环境变量自定义行为:
|
|
398
|
+
|
|
399
|
+
| 变量 | 默认值 | 说明 |
|
|
400
|
+
|------|--------|------|
|
|
401
|
+
| `JUSTONEAPI_TOKEN` | *(必需)* | 你的 JustOneAPI token |
|
|
402
|
+
| `JUSTONEAPI_BASE_URL` | `https://api.justoneapi.com` | API 端点 |
|
|
403
|
+
| `JUSTONEAPI_TIMEOUT_MS` | `20000` | 请求超时(毫秒)|
|
|
404
|
+
| `JUSTONEAPI_RETRY` | `1` | 首次尝试后的重试次数 |
|
|
405
|
+
| `JUSTONEAPI_DEBUG` | `false` | 启用调试日志到 stderr |
|
|
406
|
+
|
|
407
|
+
自定义设置示例:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"mcpServers": {
|
|
412
|
+
"justoneapi": {
|
|
413
|
+
"command": "npx",
|
|
414
|
+
"args": ["-y", "justoneapi-mcp"],
|
|
415
|
+
"env": {
|
|
416
|
+
"JUSTONEAPI_TOKEN": "你的实际token",
|
|
417
|
+
"JUSTONEAPI_TIMEOUT_MS": "30000",
|
|
418
|
+
"JUSTONEAPI_DEBUG": "true"
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 许可证
|
|
428
|
+
|
|
429
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
export const config = {
|
|
3
|
+
baseUrl: process.env.JUSTONEAPI_BASE_URL ?? "https://api.justoneapi.com",
|
|
4
|
+
token: process.env.JUSTONEAPI_TOKEN ?? "",
|
|
5
|
+
// Debug logs go to stderr so Claude Desktop can display them.
|
|
6
|
+
debug: (process.env.JUSTONEAPI_DEBUG ?? "").toLowerCase() === "true",
|
|
7
|
+
// Network behavior
|
|
8
|
+
timeoutMs: Number(process.env.JUSTONEAPI_TIMEOUT_MS ?? "20000"),
|
|
9
|
+
retry: Number(process.env.JUSTONEAPI_RETRY ?? "1"), // number of retries after the first attempt
|
|
10
|
+
};
|
|
11
|
+
export function requireToken() {
|
|
12
|
+
if (!config.token || !config.token.trim()) {
|
|
13
|
+
const err = new Error("Missing JUSTONEAPI_TOKEN. Please set it in the MCP server environment.");
|
|
14
|
+
err.upstreamCode = 100; // align with your upstream: Invalid token
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
return config.token.trim();
|
|
18
|
+
}
|
|
19
|
+
export function maskToken(t) {
|
|
20
|
+
const s = (t ?? "").trim();
|
|
21
|
+
if (!s)
|
|
22
|
+
return "";
|
|
23
|
+
if (s.length <= 8)
|
|
24
|
+
return "***";
|
|
25
|
+
return `${s.slice(0, 3)}***${s.slice(-3)}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a "safe" URL for logging:
|
|
29
|
+
* - keeps path
|
|
30
|
+
* - masks token query parameter
|
|
31
|
+
*/
|
|
32
|
+
export function toSafeUrlForLog(fullUrl) {
|
|
33
|
+
try {
|
|
34
|
+
const u = new URL(fullUrl);
|
|
35
|
+
if (u.searchParams.has("token")) {
|
|
36
|
+
u.searchParams.set("token", maskToken(u.searchParams.get("token") ?? ""));
|
|
37
|
+
}
|
|
38
|
+
return u.toString();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Fallback: best-effort masking
|
|
42
|
+
return fullUrl.replace(/token=([^&]+)/g, (_m, g1) => `token=${maskToken(g1)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function isUpstreamOk(resp) {
|
|
2
|
+
return Number(resp?.code) === 0;
|
|
3
|
+
}
|
|
4
|
+
export function mapUpstreamCode(code) {
|
|
5
|
+
switch (code) {
|
|
6
|
+
case 100:
|
|
7
|
+
return "INVALID_TOKEN";
|
|
8
|
+
case 301:
|
|
9
|
+
return "COLLECT_FAILED";
|
|
10
|
+
case 302:
|
|
11
|
+
return "RATE_LIMITED";
|
|
12
|
+
case 303:
|
|
13
|
+
return "DAILY_QUOTA_EXCEEDED";
|
|
14
|
+
case 400:
|
|
15
|
+
return "VALIDATION_ERROR";
|
|
16
|
+
case 500:
|
|
17
|
+
return "INTERNAL_ERROR";
|
|
18
|
+
case 600:
|
|
19
|
+
return "PERMISSION_DENIED";
|
|
20
|
+
case 601:
|
|
21
|
+
return "INSUFFICIENT_BALANCE";
|
|
22
|
+
default:
|
|
23
|
+
return "UPSTREAM_ERROR";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function buildUserMessage(mcpCode, upstreamMessage) {
|
|
27
|
+
const base = upstreamMessage && upstreamMessage.trim() ? upstreamMessage.trim() : undefined;
|
|
28
|
+
switch (mcpCode) {
|
|
29
|
+
case "INVALID_TOKEN":
|
|
30
|
+
return base ?? "Invalid or inactive token. Please update JUSTONEAPI_TOKEN.";
|
|
31
|
+
case "COLLECT_FAILED":
|
|
32
|
+
return base ?? "Collection failed. Please retry after a short delay.";
|
|
33
|
+
case "RATE_LIMITED":
|
|
34
|
+
return base ?? "Rate limit exceeded. Please slow down and retry later.";
|
|
35
|
+
case "DAILY_QUOTA_EXCEEDED":
|
|
36
|
+
return base ?? "Daily quota exceeded. Please try again tomorrow or upgrade your plan.";
|
|
37
|
+
case "VALIDATION_ERROR":
|
|
38
|
+
return base ?? "Invalid parameters. Please check the input values.";
|
|
39
|
+
case "PERMISSION_DENIED":
|
|
40
|
+
return base ?? "Permission denied. Please verify your account permissions.";
|
|
41
|
+
case "INSUFFICIENT_BALANCE":
|
|
42
|
+
return base ?? "Insufficient balance. Please top up your account.";
|
|
43
|
+
case "INTERNAL_ERROR":
|
|
44
|
+
return base ?? "Internal server error. Please retry later.";
|
|
45
|
+
case "NETWORK_TIMEOUT":
|
|
46
|
+
return base ?? "Network timeout. Please retry later.";
|
|
47
|
+
case "NETWORK_ERROR":
|
|
48
|
+
return base ?? "Network error. Please retry later.";
|
|
49
|
+
default:
|
|
50
|
+
return base ?? "Upstream error. Please retry later.";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function toMcpErrorPayload(e) {
|
|
54
|
+
const error = e;
|
|
55
|
+
// Timeout from AbortController
|
|
56
|
+
if (error.name === "AbortError") {
|
|
57
|
+
return { code: "NETWORK_TIMEOUT", message: buildUserMessage("NETWORK_TIMEOUT") };
|
|
58
|
+
}
|
|
59
|
+
// Our own thrown upstreamCode (business code)
|
|
60
|
+
if (error.upstreamCode !== undefined) {
|
|
61
|
+
const upstreamCode = Number(error.upstreamCode);
|
|
62
|
+
const mcpCode = mapUpstreamCode(upstreamCode);
|
|
63
|
+
return {
|
|
64
|
+
code: mcpCode,
|
|
65
|
+
message: buildUserMessage(mcpCode, error.message),
|
|
66
|
+
upstreamCode,
|
|
67
|
+
httpStatus: typeof error.httpStatus === "number" ? error.httpStatus : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// HTTP level errors if any
|
|
71
|
+
if (typeof error.httpStatus === "number") {
|
|
72
|
+
return {
|
|
73
|
+
code: "UPSTREAM_ERROR",
|
|
74
|
+
message: error.message ?? `HTTP error ${error.httpStatus}`,
|
|
75
|
+
httpStatus: error.httpStatus,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Generic network errors
|
|
79
|
+
if (error.cause || error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
80
|
+
return {
|
|
81
|
+
code: "NETWORK_ERROR",
|
|
82
|
+
message: error.message ?? buildUserMessage("NETWORK_ERROR"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { code: "UPSTREAM_ERROR", message: error.message ?? "Unknown error" };
|
|
86
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { config, requireToken, toSafeUrlForLog } from "./config.js";
|
|
2
|
+
import { isUpstreamOk } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* GET JSON helper for JustOneAPI style responses:
|
|
5
|
+
* - HTTP may be 200 even when business code != 0
|
|
6
|
+
* - Throws when:
|
|
7
|
+
* - HTTP status not OK
|
|
8
|
+
* - JSON parse fails
|
|
9
|
+
* - business code != 0
|
|
10
|
+
* Includes:
|
|
11
|
+
* - timeout
|
|
12
|
+
* - small retry
|
|
13
|
+
* - safe logging (never leaks token)
|
|
14
|
+
*/
|
|
15
|
+
export async function getJson(pathWithQuery) {
|
|
16
|
+
// ensure token exists early (so we can return INVALID_TOKEN)
|
|
17
|
+
requireToken();
|
|
18
|
+
const url = `${config.baseUrl}${pathWithQuery}`;
|
|
19
|
+
const attempts = 1 + Math.max(0, Number.isFinite(config.retry) ? config.retry : 0);
|
|
20
|
+
let lastErr;
|
|
21
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
if (config.debug) {
|
|
26
|
+
console.error(`[justoneapi] GET ${toSafeUrlForLog(url)} (attempt ${attempt}/${attempts})`);
|
|
27
|
+
}
|
|
28
|
+
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
let json;
|
|
31
|
+
try {
|
|
32
|
+
json = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
const err = new Error("Failed to parse JSON response from upstream.");
|
|
36
|
+
err.httpStatus = res.status;
|
|
37
|
+
err.payload = text;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
// HTTP-level error
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const err = new Error(json.message ?? `HTTP ${res.status}`);
|
|
43
|
+
err.httpStatus = res.status;
|
|
44
|
+
err.payload = json;
|
|
45
|
+
// If upstream also provides code, keep it
|
|
46
|
+
if (json.code !== undefined)
|
|
47
|
+
err.upstreamCode = Number(json.code);
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
// Business-level error (code != 0)
|
|
51
|
+
if (!isUpstreamOk(json)) {
|
|
52
|
+
const err = new Error(json.message ?? `Upstream code ${json.code}`);
|
|
53
|
+
err.upstreamCode = Number(json.code);
|
|
54
|
+
err.payload = json;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return json;
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
lastErr = e;
|
|
61
|
+
const error = e;
|
|
62
|
+
// Retry only for:
|
|
63
|
+
// - timeout (AbortError)
|
|
64
|
+
// - HTTP 5xx
|
|
65
|
+
// - network-type failures
|
|
66
|
+
const httpStatus = error.httpStatus;
|
|
67
|
+
const retryable = error.name === "AbortError" ||
|
|
68
|
+
(typeof httpStatus === "number" && httpStatus >= 500) ||
|
|
69
|
+
error.code === "ECONNRESET" ||
|
|
70
|
+
error.code === "ECONNREFUSED" ||
|
|
71
|
+
error.code === "ENOTFOUND";
|
|
72
|
+
if (!retryable || attempt === attempts)
|
|
73
|
+
break;
|
|
74
|
+
// small backoff
|
|
75
|
+
await new Promise((r) => setTimeout(r, 250 * attempt));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw lastErr;
|
|
82
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { toMcpErrorPayload } from "./common/errors.js";
|
|
5
|
+
import { kuaishouSearchVideoV2, KuaishouSearchVideoV2Input, } from "./tools/kuaishou/search_video_v2.js";
|
|
6
|
+
import { unifiedSearchV1, UnifiedSearchV1Input, } from "./tools/search/unified_search_v1.js";
|
|
7
|
+
import { version } from "./version.js";
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "justoneapi-mcp",
|
|
10
|
+
version,
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("kuaishou_search_video_v2", {
|
|
13
|
+
description: "Search Kuaishou videos by keyword. Returns the original raw JSON response from upstream without field normalization.",
|
|
14
|
+
inputSchema: KuaishouSearchVideoV2Input.shape,
|
|
15
|
+
}, async (input) => {
|
|
16
|
+
try {
|
|
17
|
+
const data = await kuaishouSearchVideoV2(input);
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
const m = toMcpErrorPayload(e);
|
|
24
|
+
return {
|
|
25
|
+
isError: true,
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `ERROR[${m.code}] (upstream=${m.upstreamCode ?? "N/A"}): ${m.message}`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
server.registerTool("unified_search_v1", {
|
|
36
|
+
description: "Unified search across multiple platforms (Weibo, WeChat, Zhihu, Douyin, Xiaohongshu, Bilibili, Kuaishou, News). Search by keyword with time range. Supports AND/OR/NOT operators and pagination. Returns raw JSON response.",
|
|
37
|
+
inputSchema: UnifiedSearchV1Input.shape,
|
|
38
|
+
}, async (input) => {
|
|
39
|
+
try {
|
|
40
|
+
const data = await unifiedSearchV1(input);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const m = toMcpErrorPayload(e);
|
|
47
|
+
return {
|
|
48
|
+
isError: true,
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `ERROR[${m.code}] (upstream=${m.upstreamCode ?? "N/A"}): ${m.message}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
async function main() {
|
|
59
|
+
// Validate configuration on startup
|
|
60
|
+
if (!process.env.JUSTONEAPI_TOKEN?.trim()) {
|
|
61
|
+
console.error("[justoneapi-mcp] ERROR: JUSTONEAPI_TOKEN is required but not set.\n" +
|
|
62
|
+
"Please set the JUSTONEAPI_TOKEN environment variable in your MCP host configuration.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const transport = new StdioServerTransport();
|
|
66
|
+
await server.connect(transport);
|
|
67
|
+
// Optional: log startup info to stderr (won't interfere with MCP protocol on stdout)
|
|
68
|
+
if (process.env.JUSTONEAPI_DEBUG?.toLowerCase() === "true") {
|
|
69
|
+
console.error(`[justoneapi-mcp] Server started (version ${version})`);
|
|
70
|
+
console.error(`[justoneapi-mcp] Base URL: ${process.env.JUSTONEAPI_BASE_URL ?? "https://api.justoneapi.com"}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error("[justoneapi-mcp] Fatal error:", err);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requireToken } from "../../common/config.js";
|
|
3
|
+
import { getJson } from "../../common/http.js";
|
|
4
|
+
export const KuaishouSearchVideoV2Input = z.object({
|
|
5
|
+
keyword: z.string().min(1).describe("Search keyword, e.g. 'dance'"),
|
|
6
|
+
page: z.number().int().min(1).default(1).describe("Page number, default 1"),
|
|
7
|
+
});
|
|
8
|
+
export async function kuaishouSearchVideoV2(input) {
|
|
9
|
+
const token = encodeURIComponent(requireToken());
|
|
10
|
+
const keyword = encodeURIComponent(input.keyword);
|
|
11
|
+
const page = input.page; // zod default ensures it's a number
|
|
12
|
+
return await getJson(`/api/kuaishou/search-video/v2?token=${token}&keyword=${keyword}&page=${page}`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requireToken } from "../../common/config.js";
|
|
3
|
+
import { getJson } from "../../common/http.js";
|
|
4
|
+
// Platform type enum matching CiviwPlatformType
|
|
5
|
+
export const PlatformType = z.enum([
|
|
6
|
+
"ALL",
|
|
7
|
+
"NEWS",
|
|
8
|
+
"WEIBO",
|
|
9
|
+
"WEIXIN",
|
|
10
|
+
"ZHIHU",
|
|
11
|
+
"DOUYIN",
|
|
12
|
+
"XIAOHONGSHU",
|
|
13
|
+
"BILIBILI",
|
|
14
|
+
"KUAISHOU",
|
|
15
|
+
]);
|
|
16
|
+
export const UnifiedSearchV1Input = z.object({
|
|
17
|
+
keyword: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe("Search keyword. Syntax: single keyword 'word', AND 'word1 word2', OR 'word1~word2', NOT 'word -excluded'"),
|
|
21
|
+
source: PlatformType.optional().describe("Platform source: ALL (default), NEWS, WEIBO, WEIXIN, ZHIHU, DOUYIN, XIAOHONGSHU, BILIBILI, KUAISHOU"),
|
|
22
|
+
start: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Start time in format 'yyyy-MM-dd HH:mm:ss' (UTC+8). Required for first page, must be within 84 days"),
|
|
26
|
+
end: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("End time in format 'yyyy-MM-dd HH:mm:ss' (UTC+8). Required for first page, must be after start time"),
|
|
30
|
+
nextCursor: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Pagination cursor from previous response. Use this for subsequent pages instead of start/end times"),
|
|
34
|
+
});
|
|
35
|
+
export async function unifiedSearchV1(input) {
|
|
36
|
+
const token = encodeURIComponent(requireToken());
|
|
37
|
+
const keyword = encodeURIComponent(input.keyword);
|
|
38
|
+
// Build query parameters
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
params.append("token", token);
|
|
41
|
+
params.append("keyword", keyword);
|
|
42
|
+
if (input.source) {
|
|
43
|
+
params.append("source", input.source);
|
|
44
|
+
}
|
|
45
|
+
if (input.nextCursor) {
|
|
46
|
+
// Pagination: use nextCursor
|
|
47
|
+
params.append("nextCursor", input.nextCursor);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// First page: require start and end times
|
|
51
|
+
if (!input.start || !input.end) {
|
|
52
|
+
throw new Error("start and end times are required for the first page (unless using nextCursor for pagination)");
|
|
53
|
+
}
|
|
54
|
+
params.append("start", input.start);
|
|
55
|
+
params.append("end", input.end);
|
|
56
|
+
}
|
|
57
|
+
return await getJson(`/api/search/v1?${params.toString()}`);
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requireToken } from "../common/config.js";
|
|
3
|
+
import { getJson } from "../common/http.js";
|
|
4
|
+
// Platform type enum matching CiviwPlatformType
|
|
5
|
+
export const PlatformType = z.enum([
|
|
6
|
+
"ALL",
|
|
7
|
+
"NEWS",
|
|
8
|
+
"WEIBO",
|
|
9
|
+
"WEIXIN",
|
|
10
|
+
"ZHIHU",
|
|
11
|
+
"DOUYIN",
|
|
12
|
+
"XIAOHONGSHU",
|
|
13
|
+
"BILIBILI",
|
|
14
|
+
"KUAISHOU",
|
|
15
|
+
]);
|
|
16
|
+
export const UnifiedSearchV1Input = z.object({
|
|
17
|
+
keyword: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe("Search keyword. Syntax: single keyword 'word', AND 'word1 word2', OR 'word1~word2', NOT 'word -excluded'"),
|
|
21
|
+
source: PlatformType.optional().describe("Platform source: ALL (default), NEWS, WEIBO, WEIXIN, ZHIHU, DOUYIN, XIAOHONGSHU, BILIBILI, KUAISHOU"),
|
|
22
|
+
start: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Start time in format 'yyyy-MM-dd HH:mm:ss' (UTC+8). Required for first page, must be within 84 days"),
|
|
26
|
+
end: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("End time in format 'yyyy-MM-dd HH:mm:ss' (UTC+8). Required for first page, must be after start time"),
|
|
30
|
+
nextCursor: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Pagination cursor from previous response. Use this for subsequent pages instead of start/end times"),
|
|
34
|
+
});
|
|
35
|
+
export async function unifiedSearchV1(input) {
|
|
36
|
+
const token = encodeURIComponent(requireToken());
|
|
37
|
+
const keyword = encodeURIComponent(input.keyword);
|
|
38
|
+
// Build query parameters
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
params.append("token", token);
|
|
41
|
+
params.append("keyword", keyword);
|
|
42
|
+
if (input.source) {
|
|
43
|
+
params.append("source", input.source);
|
|
44
|
+
}
|
|
45
|
+
if (input.nextCursor) {
|
|
46
|
+
// Pagination: use nextCursor
|
|
47
|
+
params.append("nextCursor", input.nextCursor);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// First page: require start and end times
|
|
51
|
+
if (!input.start || !input.end) {
|
|
52
|
+
throw new Error("start and end times are required for the first page (unless using nextCursor for pagination)");
|
|
53
|
+
}
|
|
54
|
+
params.append("start", input.start);
|
|
55
|
+
params.append("end", input.end);
|
|
56
|
+
}
|
|
57
|
+
return await getJson(`/api/search/v1?${params.toString()}`);
|
|
58
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
let version = "0.0.0";
|
|
7
|
+
try {
|
|
8
|
+
const packageJsonPath = join(__dirname, "../package.json");
|
|
9
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
10
|
+
version = packageJson.version || "0.0.0";
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
console.error("Failed to read version from package.json:", e);
|
|
14
|
+
}
|
|
15
|
+
export { version };
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "justoneapi-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for JustOneAPI - expose API endpoints to AI assistants via Model Context Protocol",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"justoneapi-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"lint": "eslint src/**/*.ts",
|
|
15
|
+
"lint:fix": "eslint src/**/*.ts --fix",
|
|
16
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
17
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"justoneapi",
|
|
24
|
+
"ai",
|
|
25
|
+
"agent",
|
|
26
|
+
"claude",
|
|
27
|
+
"cursor"
|
|
28
|
+
],
|
|
29
|
+
"author": "Just One API <support@justoneapi.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/justoneapi/justoneapi-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/justoneapi/justoneapi-mcp#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/justoneapi/justoneapi-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
49
|
+
"dotenv": "^17.2.3",
|
|
50
|
+
"zod": "^4.3.4"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.0.3",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.51.0",
|
|
56
|
+
"eslint": "^9.39.2",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"prettier": "^3.7.4",
|
|
59
|
+
"ts-node": "^10.9.2",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
61
|
+
}
|
|
62
|
+
}
|