@fietiger/joplin-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/README.md +91 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +164 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/index.ts +193 -0
- package/tsconfig.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Joplin MCP Server
|
|
2
|
+
|
|
3
|
+
这是一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 的服务器,允许 AI 模型(如 Claude)通过 Joplin 的 Web Clipper 服务与您的 [Joplin](https://joplinapp.org/) 笔记库进行交互。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
该服务器提供了以下工具:
|
|
8
|
+
|
|
9
|
+
- **get_version**: 检查与 Joplin API 的连接状态。
|
|
10
|
+
- **list_notebooks**: 列出所有笔记本。
|
|
11
|
+
- **create_note**: 创建新笔记(支持 Markdown 内容、笔记本归类及标签添加)。
|
|
12
|
+
- **search_notes**: 通过关键词搜索笔记。
|
|
13
|
+
- **get_note**: 获取特定笔记的详细内容(标题、正文等)。
|
|
14
|
+
|
|
15
|
+
## 准备工作
|
|
16
|
+
|
|
17
|
+
1. **安装 Joplin**: 确保您已经安装并运行了 Joplin 桌面客户端。
|
|
18
|
+
2. **启用 Web Clipper**:
|
|
19
|
+
- 进入 Joplin 的 `选项` -> `网页剪辑器`。
|
|
20
|
+
- 启用服务。
|
|
21
|
+
- 复制并记下您的 **授权令牌 (Token)**。
|
|
22
|
+
|
|
23
|
+
## 快速使用 (通过 npx)
|
|
24
|
+
|
|
25
|
+
如果您不想手动下载源码,可以直接通过 `npx` 在 Claude Desktop 中配置:
|
|
26
|
+
|
|
27
|
+
将以下配置添加到您的 `claude_desktop_config.json` 文件中:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"@fietiger/joplin-mcp": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "@fietiger/joplin-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"JOPLIN_TOKEN": "您的_JOPLIN_TOKEN"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 本地安装与发布
|
|
44
|
+
|
|
45
|
+
如果您是开发者并希望修改或发布此包:
|
|
46
|
+
|
|
47
|
+
### 1. 安装与编译
|
|
48
|
+
```powershell
|
|
49
|
+
npm install
|
|
50
|
+
npm run build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. 发布到 npm
|
|
54
|
+
确保您已登录 npm 账号,然后在项目根目录下执行:
|
|
55
|
+
```powershell
|
|
56
|
+
npm publish --access public
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. 本地测试运行
|
|
60
|
+
```powershell
|
|
61
|
+
$env:JOPLIN_TOKEN = "您的_JOPLIN_TOKEN"
|
|
62
|
+
node dist/index.js
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 在 Claude Desktop 中集成 (本地模式)
|
|
66
|
+
|
|
67
|
+
如果您想直接引用本地开发的路径:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"@fietiger/joplin-mcp": {
|
|
73
|
+
"command": "node",
|
|
74
|
+
"args": ["D:\\Dev2026\\01.AutoJoplinMcp\\joplin-mcp\\dist\\index.js"],
|
|
75
|
+
"env": {
|
|
76
|
+
"JOPLIN_TOKEN": "您的_JOPLIN_TOKEN"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 开发说明
|
|
84
|
+
|
|
85
|
+
- 源码位于 `src/index.ts`。
|
|
86
|
+
- 使用 `zod` 进行参数校验。
|
|
87
|
+
- 基于 `@modelcontextprotocol/sdk` 构建。
|
|
88
|
+
|
|
89
|
+
## 许可证
|
|
90
|
+
|
|
91
|
+
[ISC](LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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 { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
const JOPLIN_BASE_URL = "http://localhost:41184";
|
|
8
|
+
let JOPLIN_TOKEN = process.env.JOPLIN_TOKEN || "";
|
|
9
|
+
/**
|
|
10
|
+
* 通用 Joplin API 调用函数
|
|
11
|
+
*/
|
|
12
|
+
async function callJoplin(method, path, queryParams = {}, jsonData = null) {
|
|
13
|
+
if (!JOPLIN_TOKEN) {
|
|
14
|
+
// 尝试从命令行参数获取
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const firstArg = args[0];
|
|
17
|
+
if (firstArg) {
|
|
18
|
+
JOPLIN_TOKEN = firstArg;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!JOPLIN_TOKEN) {
|
|
22
|
+
throw new Error("JOPLIN_TOKEN 未设置,请通过环境变量 JOPLIN_TOKEN 或命令行参数传递");
|
|
23
|
+
}
|
|
24
|
+
const params = { ...queryParams, token: JOPLIN_TOKEN };
|
|
25
|
+
const url = `${JOPLIN_BASE_URL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
26
|
+
try {
|
|
27
|
+
const response = await axios({
|
|
28
|
+
method,
|
|
29
|
+
url,
|
|
30
|
+
params,
|
|
31
|
+
data: jsonData,
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
});
|
|
34
|
+
return response.data;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (axios.isAxiosError(error)) {
|
|
38
|
+
throw new McpError(ErrorCode.InternalError, `Joplin API 错误: ${error.response?.statusText || error.message}`);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 创建 McpServer 实例 (新 API)
|
|
44
|
+
const server = new McpServer({
|
|
45
|
+
name: "@fietiger/joplin-mcp",
|
|
46
|
+
version: "1.0.0",
|
|
47
|
+
});
|
|
48
|
+
// 注册 get_version 工具
|
|
49
|
+
server.tool("get_version", {}, async () => {
|
|
50
|
+
let apiStatus = "Unknown";
|
|
51
|
+
let joplinResponse = "Unknown";
|
|
52
|
+
try {
|
|
53
|
+
const results = await callJoplin("GET", "ping");
|
|
54
|
+
joplinResponse = typeof results === "string" ? results : JSON.stringify(results);
|
|
55
|
+
if (joplinResponse.includes("JoplinClipperServer")) {
|
|
56
|
+
apiStatus = "Connected";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
apiStatus = "Unexpected Response";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
apiStatus = `Error: ${error.message}`;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: JSON.stringify({
|
|
70
|
+
mcp_version: "1.0.0",
|
|
71
|
+
joplin_api_url: JOPLIN_BASE_URL,
|
|
72
|
+
joplin_api_status: apiStatus,
|
|
73
|
+
joplin_response: joplinResponse,
|
|
74
|
+
}, null, 2),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
// 注册 list_notebooks 工具
|
|
80
|
+
server.tool("list_notebooks", {}, async () => {
|
|
81
|
+
const results = await callJoplin("GET", "folders");
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: JSON.stringify(results.items || [], null, 2),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
// 注册 create_note 工具
|
|
92
|
+
server.tool("create_note", {
|
|
93
|
+
title: z.string().describe("笔记标题"),
|
|
94
|
+
body: z.string().describe("笔记内容 (Markdown 格式)"),
|
|
95
|
+
parent_id: z.string().optional().describe("笔记本 ID (可选)"),
|
|
96
|
+
tags: z.array(z.string()).optional().describe("标签列表 (可选)"),
|
|
97
|
+
}, async ({ title, body, parent_id, tags }) => {
|
|
98
|
+
const data = { title, body };
|
|
99
|
+
if (parent_id) {
|
|
100
|
+
data.parent_id = parent_id;
|
|
101
|
+
}
|
|
102
|
+
const note = await callJoplin("POST", "notes", {}, data);
|
|
103
|
+
if (tags && Array.isArray(tags) && note.id) {
|
|
104
|
+
for (const tagName of tags) {
|
|
105
|
+
const tagResults = await callJoplin("GET", "tags", { query: tagName });
|
|
106
|
+
let tagId = null;
|
|
107
|
+
if (tagResults.items && tagResults.items.length > 0) {
|
|
108
|
+
tagId = tagResults.items[0].id;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const newTag = await callJoplin("POST", "tags", {}, { title: tagName });
|
|
112
|
+
tagId = newTag.id;
|
|
113
|
+
}
|
|
114
|
+
if (tagId) {
|
|
115
|
+
await callJoplin("POST", `tags/${tagId}/notes`, {}, { id: note.id });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: JSON.stringify(note, null, 2),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// 注册 search_notes 工具
|
|
129
|
+
server.tool("search_notes", {
|
|
130
|
+
query: z.string().describe("搜索关键词"),
|
|
131
|
+
}, async ({ query }) => {
|
|
132
|
+
const results = await callJoplin("GET", "search", { query });
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: JSON.stringify(results.items || [], null, 2),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
// 注册 get_note 工具
|
|
143
|
+
server.tool("get_note", {
|
|
144
|
+
note_id: z.string().describe("笔记 ID"),
|
|
145
|
+
}, async ({ note_id }) => {
|
|
146
|
+
const result = await callJoplin("GET", `notes/${note_id}`, {
|
|
147
|
+
fields: "id,title,body,parent_id,created_time,updated_time",
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: JSON.stringify(result, null, 2),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
async function main() {
|
|
159
|
+
const transport = new StdioServerTransport();
|
|
160
|
+
await server.connect(transport);
|
|
161
|
+
console.error("Joplin MCP Server running on stdio (using McpServer API)");
|
|
162
|
+
}
|
|
163
|
+
main().catch(console.error);
|
|
164
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fietiger/joplin-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"joplin-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepare": "npm run build",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"description": "",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
21
|
+
"axios": "^1.13.2",
|
|
22
|
+
"zod": "^4.3.5"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.0.3",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
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 { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
|
|
8
|
+
const JOPLIN_BASE_URL = "http://localhost:41184";
|
|
9
|
+
let JOPLIN_TOKEN = process.env.JOPLIN_TOKEN || "";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 通用 Joplin API 调用函数
|
|
13
|
+
*/
|
|
14
|
+
async function callJoplin(method: string, path: string, queryParams: any = {}, jsonData: any = null) {
|
|
15
|
+
if (!JOPLIN_TOKEN) {
|
|
16
|
+
// 尝试从命令行参数获取
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const firstArg = args[0];
|
|
19
|
+
if (firstArg) {
|
|
20
|
+
JOPLIN_TOKEN = firstArg;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!JOPLIN_TOKEN) {
|
|
25
|
+
throw new Error("JOPLIN_TOKEN 未设置,请通过环境变量 JOPLIN_TOKEN 或命令行参数传递");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const params = { ...queryParams, token: JOPLIN_TOKEN };
|
|
29
|
+
const url = `${JOPLIN_BASE_URL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await axios({
|
|
33
|
+
method,
|
|
34
|
+
url,
|
|
35
|
+
params,
|
|
36
|
+
data: jsonData,
|
|
37
|
+
timeout: 10000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return response.data;
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
if (axios.isAxiosError(error)) {
|
|
43
|
+
throw new McpError(
|
|
44
|
+
ErrorCode.InternalError,
|
|
45
|
+
`Joplin API 错误: ${error.response?.statusText || error.message}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 创建 McpServer 实例 (新 API)
|
|
53
|
+
const server = new McpServer({
|
|
54
|
+
name: "@fietiger/joplin-mcp",
|
|
55
|
+
version: "1.0.0",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 注册 get_version 工具
|
|
59
|
+
server.tool("get_version", {}, async () => {
|
|
60
|
+
let apiStatus = "Unknown";
|
|
61
|
+
let joplinResponse = "Unknown";
|
|
62
|
+
try {
|
|
63
|
+
const results = await callJoplin("GET", "ping");
|
|
64
|
+
joplinResponse = typeof results === "string" ? results : JSON.stringify(results);
|
|
65
|
+
if (joplinResponse.includes("JoplinClipperServer")) {
|
|
66
|
+
apiStatus = "Connected";
|
|
67
|
+
} else {
|
|
68
|
+
apiStatus = "Unexpected Response";
|
|
69
|
+
}
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
apiStatus = `Error: ${error.message}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: JSON.stringify({
|
|
79
|
+
mcp_version: "1.0.0",
|
|
80
|
+
joplin_api_url: JOPLIN_BASE_URL,
|
|
81
|
+
joplin_api_status: apiStatus,
|
|
82
|
+
joplin_response: joplinResponse,
|
|
83
|
+
}, null, 2),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 注册 list_notebooks 工具
|
|
90
|
+
server.tool("list_notebooks", {}, async () => {
|
|
91
|
+
const results = await callJoplin("GET", "folders");
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: JSON.stringify(results.items || [], null, 2),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 注册 create_note 工具
|
|
103
|
+
server.tool(
|
|
104
|
+
"create_note",
|
|
105
|
+
{
|
|
106
|
+
title: z.string().describe("笔记标题"),
|
|
107
|
+
body: z.string().describe("笔记内容 (Markdown 格式)"),
|
|
108
|
+
parent_id: z.string().optional().describe("笔记本 ID (可选)"),
|
|
109
|
+
tags: z.array(z.string()).optional().describe("标签列表 (可选)"),
|
|
110
|
+
},
|
|
111
|
+
async ({ title, body, parent_id, tags }) => {
|
|
112
|
+
const data: any = { title, body };
|
|
113
|
+
if (parent_id) {
|
|
114
|
+
data.parent_id = parent_id;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const note = await callJoplin("POST", "notes", {}, data);
|
|
118
|
+
|
|
119
|
+
if (tags && Array.isArray(tags) && note.id) {
|
|
120
|
+
for (const tagName of tags) {
|
|
121
|
+
const tagResults = await callJoplin("GET", "tags", { query: tagName });
|
|
122
|
+
let tagId: string | null = null;
|
|
123
|
+
if (tagResults.items && tagResults.items.length > 0) {
|
|
124
|
+
tagId = tagResults.items[0].id;
|
|
125
|
+
} else {
|
|
126
|
+
const newTag = await callJoplin("POST", "tags", {}, { title: tagName });
|
|
127
|
+
tagId = newTag.id;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (tagId) {
|
|
131
|
+
await callJoplin("POST", `tags/${tagId}/notes`, {}, { id: note.id });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify(note, null, 2),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// 注册 search_notes 工具
|
|
148
|
+
server.tool(
|
|
149
|
+
"search_notes",
|
|
150
|
+
{
|
|
151
|
+
query: z.string().describe("搜索关键词"),
|
|
152
|
+
},
|
|
153
|
+
async ({ query }) => {
|
|
154
|
+
const results = await callJoplin("GET", "search", { query });
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: JSON.stringify(results.items || [], null, 2),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// 注册 get_note 工具
|
|
167
|
+
server.tool(
|
|
168
|
+
"get_note",
|
|
169
|
+
{
|
|
170
|
+
note_id: z.string().describe("笔记 ID"),
|
|
171
|
+
},
|
|
172
|
+
async ({ note_id }) => {
|
|
173
|
+
const result = await callJoplin("GET", `notes/${note_id}`, {
|
|
174
|
+
fields: "id,title,body,parent_id,created_time,updated_time",
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: JSON.stringify(result, null, 2),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
async function main() {
|
|
188
|
+
const transport = new StdioServerTransport();
|
|
189
|
+
await server.connect(transport);
|
|
190
|
+
console.error("Joplin MCP Server running on stdio (using McpServer API)");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch(console.error);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
"module": "NodeNext",
|
|
10
|
+
"moduleResolution": "NodeNext",
|
|
11
|
+
"target": "ESNext",
|
|
12
|
+
"lib": ["ESNext"],
|
|
13
|
+
"types": ["node"],
|
|
14
|
+
// For nodejs:
|
|
15
|
+
// "lib": ["esnext"],
|
|
16
|
+
// "types": ["node"],
|
|
17
|
+
// and npm install -D @types/node
|
|
18
|
+
|
|
19
|
+
// Other Outputs
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"declarationMap": true,
|
|
23
|
+
|
|
24
|
+
// Stricter Typechecking Options
|
|
25
|
+
"noUncheckedIndexedAccess": true,
|
|
26
|
+
"exactOptionalPropertyTypes": true,
|
|
27
|
+
|
|
28
|
+
// Style Options
|
|
29
|
+
// "noImplicitReturns": true,
|
|
30
|
+
// "noImplicitOverride": true,
|
|
31
|
+
// "noUnusedLocals": true,
|
|
32
|
+
// "noUnusedParameters": true,
|
|
33
|
+
// "noFallthroughCasesInSwitch": true,
|
|
34
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
35
|
+
|
|
36
|
+
// Recommended Options
|
|
37
|
+
"strict": true,
|
|
38
|
+
"jsx": "react-jsx",
|
|
39
|
+
"verbatimModuleSyntax": true,
|
|
40
|
+
"isolatedModules": true,
|
|
41
|
+
"noUncheckedSideEffectImports": true,
|
|
42
|
+
"moduleDetection": "force",
|
|
43
|
+
"skipLibCheck": true,
|
|
44
|
+
}
|
|
45
|
+
}
|