get_notebook_mcp_server 1.0.1

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 ADDED
@@ -0,0 +1,81 @@
1
+ # Get Notes MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for integrating with Get Notes API. This server provides tools to search and recall knowledge from your Get Notes knowledge base.
4
+
5
+ ## Features
6
+
7
+ - **Knowledge Search**: AI-processed search that returns synthesized answers and references.
8
+ - **Knowledge Recall**: Raw recall of relevant notes and files.
9
+ - **Rate Limiting**: Built-in protection with QPS < 2 and Total Requests < 5000 limits.
10
+ - **Retry Mechanism**: Automatic retries for transient network errors (5xx).
11
+
12
+ ## Installation
13
+
14
+ 1. Clone the repository
15
+ 2. Install dependencies:
16
+ ```bash
17
+ npm install
18
+ ```
19
+ 3. Create `.env` file from example:
20
+ ```bash
21
+ cp .env.example .env
22
+ ```
23
+ 4. Configure your API key in `.env`:
24
+ ```
25
+ GET_API_KEY=your_api_key_here
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Running the Server
31
+
32
+ ```bash
33
+ node index.js
34
+ ```
35
+
36
+ ### Testing with MCP Inspector
37
+
38
+ You can test the MCP server interactively using the MCP Inspector:
39
+
40
+ ```bash
41
+ npx @modelcontextprotocol/inspector node index.js
42
+ ```
43
+
44
+ ### Tools
45
+
46
+ #### `search_knowledge`
47
+ Search the knowledge base with AI processing.
48
+
49
+ **Parameters:**
50
+ - `question` (string, required): The question to ask.
51
+ - `topic_ids` (array<string>, required): List of knowledge base IDs.
52
+ - `deep_seek` (boolean): Enable deep thinking mode (default: true).
53
+ - `history` (array): Chat history for context.
54
+
55
+ #### `recall_knowledge`
56
+ Raw recall from knowledge base without AI synthesis.
57
+
58
+ **Parameters:**
59
+ - `question` (string, required): The question or query.
60
+ - `topic_id` (string, required): Knowledge base ID.
61
+ - `top_k` (number): Number of results to return (default: 10).
62
+ - `intent_rewrite` (boolean): Enable intent rewrite (default: false).
63
+
64
+ ## Development
65
+
66
+ ### Running Tests
67
+
68
+ ```bash
69
+ npm test
70
+ ```
71
+
72
+ ### Project Structure
73
+
74
+ - `src/api`: API client implementation
75
+ - `src/utils`: Utility classes (RateLimiter, etc.)
76
+ - `tests`: Unit and integration tests
77
+ - `index.js`: Main MCP server entry point
78
+
79
+ ## License
80
+
81
+ ISC
package/index.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+ import { z } from 'zod';
9
+ import dotenv from 'dotenv';
10
+ import { ApiClient } from './src/api/client.js';
11
+ import logger from './src/logger.js';
12
+
13
+ dotenv.config();
14
+
15
+ const API_KEY = process.env.GET_API_KEY;
16
+ if (!API_KEY) {
17
+ logger.error('GET_API_KEY environment variable is required');
18
+ process.exit(1);
19
+ }
20
+
21
+ const apiClient = new ApiClient({ apiKey: API_KEY });
22
+
23
+ const server = new Server(
24
+ {
25
+ name: 'get-notes-mcp',
26
+ version: '1.0.0',
27
+ },
28
+ {
29
+ capabilities: {
30
+ tools: {},
31
+ },
32
+ }
33
+ );
34
+
35
+ /**
36
+ * Tool Definitions
37
+ */
38
+ const SEARCH_KNOWLEDGE_TOOL = {
39
+ name: 'search_knowledge',
40
+ description: 'Search knowledge base with AI processing. Returns synthesized answers and references.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ question: {
45
+ type: 'string',
46
+ description: 'The question to ask'
47
+ },
48
+ topic_ids: {
49
+ type: 'array',
50
+ items: { type: 'string' },
51
+ description: 'List of knowledge base IDs (currently supports only 1)'
52
+ },
53
+ deep_seek: {
54
+ type: 'boolean',
55
+ description: 'Enable deep thinking mode',
56
+ default: true
57
+ },
58
+ history: {
59
+ type: 'array',
60
+ description: 'Chat history for context',
61
+ items: {
62
+ type: 'object',
63
+ properties: {
64
+ content: { type: 'string' },
65
+ role: { type: 'string', enum: ['user', 'assistant'] }
66
+ }
67
+ }
68
+ }
69
+ },
70
+ required: ['question', 'topic_ids']
71
+ }
72
+ };
73
+
74
+ const RECALL_KNOWLEDGE_TOOL = {
75
+ name: 'recall_knowledge',
76
+ description: 'Raw recall from knowledge base without AI synthesis. Returns list of relevant notes/files.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ question: {
81
+ type: 'string',
82
+ description: 'The question or query'
83
+ },
84
+ topic_id: {
85
+ type: 'string',
86
+ description: 'Knowledge base ID'
87
+ },
88
+ top_k: {
89
+ type: 'number',
90
+ description: 'Number of results to return',
91
+ default: 10
92
+ },
93
+ intent_rewrite: {
94
+ type: 'boolean',
95
+ description: 'Enable intent rewrite',
96
+ default: false
97
+ }
98
+ },
99
+ required: ['question', 'topic_id']
100
+ }
101
+ };
102
+
103
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
104
+ return {
105
+ tools: [SEARCH_KNOWLEDGE_TOOL, RECALL_KNOWLEDGE_TOOL],
106
+ };
107
+ });
108
+
109
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
110
+ try {
111
+ const { name, arguments: args } = request.params;
112
+
113
+ switch (name) {
114
+ case 'search_knowledge': {
115
+ const params = args;
116
+ // Ensure topic_ids is array
117
+ if (!Array.isArray(params.topic_ids)) {
118
+ throw new Error('topic_ids must be an array');
119
+ }
120
+
121
+ const response = await apiClient.searchKnowledge(params);
122
+ // Handle stream or json response. For MCP tool, we typically wait for full response or handle stream if supported.
123
+ // The API guide says /stream is optional path, but client uses post /knowledge/search.
124
+ // If it returns stream, we might need to buffer it or use the non-stream version.
125
+ // For simplicity in this version, we assume non-stream JSON response unless stream param is explicitly handled.
126
+ // Actually the API guide shows stream response format. Let's assume we want the final answer.
127
+ // If the API returns a stream, axios might return a stream object.
128
+
129
+ // For now, let's assume standard JSON response if stream is not set to true in params.
130
+ // If the user wants stream, we might need a different approach, but MCP tools usually return text.
131
+
132
+ return {
133
+ content: [
134
+ {
135
+ type: 'text',
136
+ text: JSON.stringify(response.data, null, 2),
137
+ },
138
+ ],
139
+ };
140
+ }
141
+
142
+ case 'recall_knowledge': {
143
+ const params = args;
144
+ const response = await apiClient.recallKnowledge(params);
145
+ return {
146
+ content: [
147
+ {
148
+ type: 'text',
149
+ text: JSON.stringify(response.data, null, 2),
150
+ },
151
+ ],
152
+ };
153
+ }
154
+
155
+ default:
156
+ throw new Error(`Unknown tool: ${name}`);
157
+ }
158
+ } catch (error) {
159
+ logger.error('Tool execution error', { error: error.message });
160
+ return {
161
+ content: [
162
+ {
163
+ type: 'text',
164
+ text: `Error: ${error.message}`,
165
+ },
166
+ ],
167
+ isError: true,
168
+ };
169
+ }
170
+ });
171
+
172
+ async function runServer() {
173
+ const transport = new StdioServerTransport();
174
+ await server.connect(transport);
175
+ logger.info('Get Notes MCP Server running on stdio');
176
+ }
177
+
178
+ runServer().catch((error) => {
179
+ logger.error('Fatal error running server', { error });
180
+ process.exit(1);
181
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "get_notebook_mcp_server",
3
+ "version": "1.0.1",
4
+ "description": "MCP server for Get Notes API integration",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "get-notes-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "get-notes",
21
+ "ai"
22
+ ],
23
+ "author": "",
24
+ "license": "ISC",
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.22.0",
27
+ "axios": "^1.13.2",
28
+ "dotenv": "^17.2.3",
29
+ "winston": "^3.18.3",
30
+ "zod": "^4.1.12"
31
+ },
32
+ "devDependencies": {
33
+ "jest": "^30.2.0"
34
+ }
35
+ }
@@ -0,0 +1,91 @@
1
+ import axios from 'axios';
2
+ import { globalRateLimiter } from '../utils/rateLimiter.js';
3
+ import logger from '../logger.js';
4
+
5
+ export class ApiClient {
6
+ constructor(config) {
7
+ this.baseUrl = config.baseUrl || 'https://open-api.biji.com/getnote/openapi';
8
+ this.apiKey = config.apiKey;
9
+
10
+ if (!this.apiKey) {
11
+ throw new Error('API Key is required');
12
+ }
13
+
14
+ this.client = axios.create({
15
+ baseURL: this.baseUrl,
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ 'Connection': 'keep-alive',
19
+ 'Authorization': `Bearer ${this.apiKey}`,
20
+ 'X-OAuth-Version': '1'
21
+ }
22
+ });
23
+
24
+ // Add response interceptor for error logging
25
+ this.client.interceptors.response.use(
26
+ response => response,
27
+ error => {
28
+ logger.error('API Request Failed:', {
29
+ url: error.config?.url,
30
+ status: error.response?.status,
31
+ data: error.response?.data,
32
+ message: error.message
33
+ });
34
+ return Promise.reject(error);
35
+ }
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Execute a request with rate limiting and retries
41
+ * @param {Function} requestFn
42
+ * @param {number} retries
43
+ */
44
+ async executeRequest(requestFn, retries = 3) {
45
+ return globalRateLimiter.schedule(async () => {
46
+ let lastError;
47
+ for (let i = 0; i < retries; i++) {
48
+ try {
49
+ return await requestFn();
50
+ } catch (error) {
51
+ lastError = error;
52
+ const isRetryable = !error.response || (error.response.status >= 500 && error.response.status < 600) || error.code === 'ECONNABORTED';
53
+
54
+ if (!isRetryable || i === retries - 1) {
55
+ throw error;
56
+ }
57
+
58
+ // Exponential backoff: 1000ms, 2000ms, 4000ms
59
+ const delay = 1000 * Math.pow(2, i);
60
+ logger.warn(`Request failed, retrying in ${delay}ms... (${i + 1}/${retries})`);
61
+ await new Promise(resolve => setTimeout(resolve, delay));
62
+ }
63
+ }
64
+ throw lastError;
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Knowledge Search (AI Processed)
70
+ * @param {Object} params
71
+ * @returns {Promise<any>}
72
+ */
73
+ async searchKnowledge(params) {
74
+ return this.executeRequest(() =>
75
+ this.client.post('/knowledge/search', params, {
76
+ responseType: params.stream ? 'stream' : 'json'
77
+ })
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Knowledge Recall (Raw)
83
+ * @param {Object} params
84
+ * @returns {Promise<any>}
85
+ */
86
+ async recallKnowledge(params) {
87
+ return this.executeRequest(() =>
88
+ this.client.post('/knowledge/search/recall', params)
89
+ );
90
+ }
91
+ }
package/src/logger.js ADDED
@@ -0,0 +1,19 @@
1
+ import winston from 'winston';
2
+
3
+ const logger = winston.createLogger({
4
+ level: 'info',
5
+ format: winston.format.combine(
6
+ winston.format.timestamp(),
7
+ winston.format.json()
8
+ ),
9
+ transports: [
10
+ new winston.transports.Console({
11
+ format: winston.format.combine(
12
+ winston.format.colorize(),
13
+ winston.format.simple()
14
+ )
15
+ })
16
+ ]
17
+ });
18
+
19
+ export default logger;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Rate Limiter implementation
3
+ * Limits requests to:
4
+ * - QPS < 2 (Minimum 500ms interval between requests)
5
+ * - Total requests < 5000
6
+ */
7
+ export class RateLimiter {
8
+ constructor(maxQps = 2, maxTotal = 5000) {
9
+ this.minInterval = 1000 / maxQps; // Minimum interval in ms
10
+ this.maxTotal = maxTotal;
11
+ this.requestCount = 0;
12
+ this.lastRequestTime = 0;
13
+ this.queue = [];
14
+ this.processing = false;
15
+ }
16
+
17
+ /**
18
+ * Execute a function with rate limiting
19
+ * @param {Function} fn - The async function to execute
20
+ * @returns {Promise<any>} - The result of the function
21
+ */
22
+ async schedule(fn) {
23
+ if (this.requestCount >= this.maxTotal) {
24
+ throw new Error(`Total request limit of ${this.maxTotal} exceeded.`);
25
+ }
26
+
27
+ return new Promise((resolve, reject) => {
28
+ this.queue.push({ fn, resolve, reject });
29
+ this.processQueue();
30
+ });
31
+ }
32
+
33
+ async processQueue() {
34
+ if (this.processing || this.queue.length === 0) {
35
+ return;
36
+ }
37
+
38
+ this.processing = true;
39
+
40
+ while (this.queue.length > 0) {
41
+ if (this.requestCount >= this.maxTotal) {
42
+ const { reject } = this.queue.shift();
43
+ reject(new Error(`Total request limit of ${this.maxTotal} exceeded.`));
44
+ continue;
45
+ }
46
+
47
+ const now = Date.now();
48
+ const timeSinceLastRequest = now - this.lastRequestTime;
49
+
50
+ if (timeSinceLastRequest < this.minInterval) {
51
+ const waitTime = this.minInterval - timeSinceLastRequest;
52
+ await new Promise(resolve => setTimeout(resolve, waitTime));
53
+ }
54
+
55
+ const { fn, resolve, reject } = this.queue.shift();
56
+
57
+ this.lastRequestTime = Date.now();
58
+ this.requestCount++;
59
+
60
+ try {
61
+ const result = await fn();
62
+ resolve(result);
63
+ } catch (error) {
64
+ reject(error);
65
+ }
66
+ }
67
+
68
+ this.processing = false;
69
+ }
70
+
71
+ /**
72
+ * Get current stats
73
+ */
74
+ getStats() {
75
+ return {
76
+ requestCount: this.requestCount,
77
+ queueLength: this.queue.length
78
+ };
79
+ }
80
+ }
81
+
82
+ export const globalRateLimiter = new RateLimiter(2, 5000);