obsidian-mcp-server 1.3.0 → 1.4.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/build/index.js +12 -3
- package/build/mcp/handlers.js +138 -0
- package/build/mcp/index.js +7 -0
- package/build/mcp/server.js +131 -0
- package/build/mcp/types.js +7 -0
- package/build/{obsidian.js → obsidian/client.js} +116 -96
- package/build/obsidian/errors.js +75 -0
- package/build/obsidian/index.js +7 -0
- package/build/obsidian/types.js +12 -0
- package/build/resources/index.js +15 -0
- package/build/{resources.js → resources/tags.js} +31 -5
- package/build/resources/types.js +5 -0
- package/build/tools/base.js +78 -0
- package/build/tools/files/content.js +171 -0
- package/build/tools/files/index.js +22 -0
- package/build/tools/files/list.js +133 -0
- package/build/tools/index.js +31 -0
- package/build/tools/properties/index.js +19 -0
- package/build/{properties.js → tools/properties/manager.js} +40 -8
- package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
- package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
- package/build/tools/search/complex.js +203 -0
- package/build/tools/search/index.js +20 -0
- package/build/tools/search/simple.js +127 -0
- package/build/utils/errors.js +59 -0
- package/build/utils/index.js +9 -0
- package/build/utils/logging.js +119 -0
- package/build/utils/rate-limiting.js +94 -0
- package/build/utils/tokenization.js +62 -0
- package/build/utils/validation.js +88 -0
- package/examples/README.md +48 -0
- package/examples/append-content.md +63 -0
- package/examples/complex-search.md +117 -0
- package/examples/find-in-file.md +94 -0
- package/examples/get-file-contents.md +72 -0
- package/examples/get-properties.md +89 -0
- package/examples/list-files-in-dir.md +55 -0
- package/examples/list-files-in-vault.md +53 -0
- package/examples/patch-content.md +60 -0
- package/examples/update-properties.md +126 -0
- package/package.json +1 -1
- package/src/index.ts +13 -3
- package/src/mcp/handlers.ts +183 -0
- package/src/mcp/index.ts +6 -0
- package/src/mcp/server.ts +162 -0
- package/src/mcp/types.ts +46 -0
- package/src/{obsidian.ts → obsidian/client.ts} +132 -125
- package/src/obsidian/errors.ts +105 -0
- package/src/obsidian/index.ts +6 -0
- package/src/obsidian/types.ts +124 -0
- package/src/resources/index.ts +17 -0
- package/src/{resources.ts → resources/tags.ts} +38 -6
- package/src/resources/types.ts +30 -0
- package/src/tools/base.ts +112 -0
- package/src/tools/files/content.ts +206 -0
- package/src/tools/files/index.ts +31 -0
- package/src/tools/files/list.ts +150 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/properties/index.ts +21 -0
- package/src/{properties.ts → tools/properties/manager.ts} +41 -8
- package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
- package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
- package/src/tools/search/complex.ts +231 -0
- package/src/tools/search/index.ts +22 -0
- package/src/tools/search/simple.ts +147 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logging.ts +146 -0
- package/src/utils/rate-limiting.ts +114 -0
- package/src/utils/tokenization.ts +71 -0
- package/src/utils/validation.ts +95 -0
- package/build/server.js +0 -263
- package/build/tools.js +0 -845
- package/build/types.js +0 -37
- package/src/server.ts +0 -335
- package/src/tools.ts +0 -926
- package/src/types.ts +0 -184
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property types and schemas for Obsidian notes
|
|
3
|
+
*/
|
|
1
4
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Define validation schemas
|
|
7
|
+
* Allow any string for type to be more flexible
|
|
8
|
+
*/
|
|
4
9
|
export const PropertyType = z.string();
|
|
10
|
+
/**
|
|
11
|
+
* Valid status values for notes
|
|
12
|
+
*/
|
|
5
13
|
export const StatusEnum = z.enum([
|
|
6
14
|
"draft",
|
|
7
15
|
"in-progress",
|
|
8
16
|
"review",
|
|
9
17
|
"complete"
|
|
10
18
|
]);
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Schema for reading properties (includes timestamps)
|
|
21
|
+
*/
|
|
12
22
|
export const ObsidianPropertiesSchema = z.object({
|
|
13
23
|
// Basic Metadata
|
|
14
24
|
// Note: Timestamps are managed automatically
|
|
@@ -32,7 +42,9 @@ export const ObsidianPropertiesSchema = z.object({
|
|
|
32
42
|
// Custom Fields
|
|
33
43
|
custom: z.record(z.unknown()).optional()
|
|
34
44
|
});
|
|
35
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Schema for validating property updates (excludes timestamps)
|
|
47
|
+
*/
|
|
36
48
|
export const PropertyUpdateSchema = z.object({
|
|
37
49
|
// Basic Metadata
|
|
38
50
|
title: z.string().optional(),
|
|
@@ -54,4 +66,4 @@ export const PropertyUpdateSchema = z.object({
|
|
|
54
66
|
// Custom Fields
|
|
55
67
|
custom: z.record(z.unknown()).optional()
|
|
56
68
|
});
|
|
57
|
-
//# sourceMappingURL=
|
|
69
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { PropertyManager } from "../properties/manager.js";
|
|
2
|
+
import { BaseToolHandler } from "../base.js";
|
|
3
|
+
import { createLogger } from "../../utils/logging.js";
|
|
4
|
+
// Create a logger for complex search operations
|
|
5
|
+
const logger = createLogger('ComplexSearchTools');
|
|
6
|
+
/**
|
|
7
|
+
* Tool name for complex search
|
|
8
|
+
*/
|
|
9
|
+
export const COMPLEX_SEARCH_TOOL_NAME = "obsidian_complex_search";
|
|
10
|
+
/**
|
|
11
|
+
* Tool handler for complex JsonLogic searches
|
|
12
|
+
*/
|
|
13
|
+
export class ComplexSearchToolHandler extends BaseToolHandler {
|
|
14
|
+
constructor(client) {
|
|
15
|
+
super(COMPLEX_SEARCH_TOOL_NAME, client);
|
|
16
|
+
}
|
|
17
|
+
getToolDescription() {
|
|
18
|
+
return {
|
|
19
|
+
name: this.name,
|
|
20
|
+
description: "File path pattern matching using JsonLogic queries. Supported operations:\n- glob: Pattern matching for paths (e.g., \"*.md\")\n- Variable access: {\"var\": \"path\"}\n\nNote: For full-text content search, date-based searches, or other advanced queries, use obsidian_find_in_file instead.",
|
|
21
|
+
examples: [
|
|
22
|
+
{
|
|
23
|
+
description: "Find markdown files in Projects folder",
|
|
24
|
+
args: {
|
|
25
|
+
query: {
|
|
26
|
+
"glob": ["Projects/*.md", { "var": "path" }]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
description: "Find files in a specific subfolder",
|
|
32
|
+
args: {
|
|
33
|
+
query: {
|
|
34
|
+
"glob": ["**/Test/*.md", { "var": "path" }]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
query: {
|
|
43
|
+
type: "object",
|
|
44
|
+
description: "JsonLogic query object. Example: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
required: ["query"]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async runTool(args) {
|
|
52
|
+
try {
|
|
53
|
+
logger.debug(`Executing complex search with query: ${JSON.stringify(args.query)}`);
|
|
54
|
+
// Perform search
|
|
55
|
+
const results = await this.client.searchJson(args.query);
|
|
56
|
+
// Format response based on result type
|
|
57
|
+
const formattedResults = results.map(result => {
|
|
58
|
+
if ('matches' in result) {
|
|
59
|
+
// SimpleSearchResult
|
|
60
|
+
return {
|
|
61
|
+
filename: result.filename,
|
|
62
|
+
matches: result.matches,
|
|
63
|
+
score: result.score
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// SearchResult
|
|
68
|
+
return {
|
|
69
|
+
filename: result.filename,
|
|
70
|
+
result: result.result
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
logger.debug(`Complex search found ${results.length} results`);
|
|
75
|
+
return this.createResponse({
|
|
76
|
+
message: `Found ${results.length} result(s)`,
|
|
77
|
+
results: formattedResults
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error(`Complex search error: ${error}`);
|
|
82
|
+
return this.handleError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tool handler for getting all tags used in the vault
|
|
88
|
+
*/
|
|
89
|
+
export class GetTagsToolHandler extends BaseToolHandler {
|
|
90
|
+
propertyManager;
|
|
91
|
+
constructor(client) {
|
|
92
|
+
super("obsidian_get_tags", client);
|
|
93
|
+
this.propertyManager = new PropertyManager(client);
|
|
94
|
+
}
|
|
95
|
+
getToolDescription() {
|
|
96
|
+
return {
|
|
97
|
+
name: this.name,
|
|
98
|
+
description: "Get all tags used across the Obsidian vault with their usage counts. Optionally filter tags within a specific folder.",
|
|
99
|
+
examples: [
|
|
100
|
+
{
|
|
101
|
+
description: "Get all tags in vault",
|
|
102
|
+
args: {}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
description: "Get tags in Projects folder",
|
|
106
|
+
args: {
|
|
107
|
+
path: "Projects"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
description: "Example response",
|
|
112
|
+
args: {},
|
|
113
|
+
response: {
|
|
114
|
+
"tags": [
|
|
115
|
+
{
|
|
116
|
+
"name": "project",
|
|
117
|
+
"count": 15,
|
|
118
|
+
"files": [
|
|
119
|
+
"Projects/ProjectA.md",
|
|
120
|
+
"Projects/ProjectB.md"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"metadata": {
|
|
125
|
+
"totalOccurrences": 45,
|
|
126
|
+
"uniqueTags": 12,
|
|
127
|
+
"scannedFiles": 30
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
path: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Optional path to limit tag search to specific folder",
|
|
138
|
+
format: "path"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async runTool(args) {
|
|
145
|
+
try {
|
|
146
|
+
logger.debug(`Getting tags${args.path ? ` in path: ${args.path}` : ' in whole vault'}`);
|
|
147
|
+
const tagMap = new Map();
|
|
148
|
+
// Use searchJson to find files with .md extension
|
|
149
|
+
const query = args.path
|
|
150
|
+
? { "glob": [`${args.path}/**/*.md`.replace(/\\/g, '/'), { "var": "path" }] }
|
|
151
|
+
: { "glob": ["**/*.md", { "var": "path" }] };
|
|
152
|
+
const results = await this.client.searchJson(query);
|
|
153
|
+
let scannedFiles = 0;
|
|
154
|
+
// Process each file to extract tags from frontmatter
|
|
155
|
+
for (const result of results) {
|
|
156
|
+
if (!('filename' in result))
|
|
157
|
+
continue;
|
|
158
|
+
try {
|
|
159
|
+
const content = await this.client.getFileContents(result.filename);
|
|
160
|
+
scannedFiles++;
|
|
161
|
+
// Use PropertyManager to properly parse frontmatter
|
|
162
|
+
const properties = this.propertyManager.parseProperties(content);
|
|
163
|
+
// Process tags if they exist
|
|
164
|
+
if (properties.tags && Array.isArray(properties.tags)) {
|
|
165
|
+
// Process each tag
|
|
166
|
+
for (const tag of properties.tags) {
|
|
167
|
+
const cleanTag = tag.replace(/^#/, ''); // Remove leading # if present
|
|
168
|
+
if (!tagMap.has(cleanTag)) {
|
|
169
|
+
tagMap.set(cleanTag, new Set());
|
|
170
|
+
}
|
|
171
|
+
tagMap.get(cleanTag).add(result.filename);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
logger.error(`Failed to process file ${result.filename}:`, error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Calculate total occurrences
|
|
180
|
+
const totalOccurrences = Array.from(tagMap.values())
|
|
181
|
+
.reduce((sum, files) => sum + files.size, 0);
|
|
182
|
+
return this.createResponse({
|
|
183
|
+
tags: Array.from(tagMap.entries())
|
|
184
|
+
.map(([name, files]) => ({
|
|
185
|
+
name,
|
|
186
|
+
count: files.size,
|
|
187
|
+
files: Array.from(files).sort()
|
|
188
|
+
}))
|
|
189
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
190
|
+
metadata: {
|
|
191
|
+
totalOccurrences,
|
|
192
|
+
uniqueTags: tagMap.size,
|
|
193
|
+
scannedFiles,
|
|
194
|
+
lastUpdate: Date.now()
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return this.handleError(error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=complex.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search tools exports
|
|
3
|
+
*/
|
|
4
|
+
export * from './simple.js';
|
|
5
|
+
export * from './complex.js';
|
|
6
|
+
import { FindInFileToolHandler } from './simple.js';
|
|
7
|
+
import { ComplexSearchToolHandler, GetTagsToolHandler } from './complex.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create all search-related tool handlers
|
|
10
|
+
* @param client The ObsidianClient instance
|
|
11
|
+
* @returns Array of search tool handlers
|
|
12
|
+
*/
|
|
13
|
+
export function createSearchToolHandlers(client) {
|
|
14
|
+
return [
|
|
15
|
+
new FindInFileToolHandler(client),
|
|
16
|
+
new ComplexSearchToolHandler(client),
|
|
17
|
+
new GetTagsToolHandler(client)
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BaseToolHandler } from "../base.js";
|
|
2
|
+
import { createLogger } from "../../utils/logging.js";
|
|
3
|
+
// Create a logger for search operations
|
|
4
|
+
const logger = createLogger('SimpleSearchTools');
|
|
5
|
+
/**
|
|
6
|
+
* Tool name for simple search
|
|
7
|
+
*/
|
|
8
|
+
export const SIMPLE_SEARCH_TOOL_NAME = "obsidian_find_in_file";
|
|
9
|
+
/**
|
|
10
|
+
* Tool handler for simple text search across files
|
|
11
|
+
*/
|
|
12
|
+
export class FindInFileToolHandler extends BaseToolHandler {
|
|
13
|
+
constructor(client) {
|
|
14
|
+
super(SIMPLE_SEARCH_TOOL_NAME, client);
|
|
15
|
+
}
|
|
16
|
+
getToolDescription() {
|
|
17
|
+
return {
|
|
18
|
+
name: this.name,
|
|
19
|
+
description: "Full-text search across all files in the vault. Returns matching files with surrounding context for each match. For results with more than 5 matching files, returns only file names and match counts to prevent overwhelming responses. Useful for finding specific content, references, or patterns across notes.",
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
description: "Search for a specific term",
|
|
23
|
+
args: {
|
|
24
|
+
query: "neural networks",
|
|
25
|
+
contextLength: 20
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
description: "Search with default context",
|
|
30
|
+
args: {
|
|
31
|
+
query: "#todo"
|
|
32
|
+
},
|
|
33
|
+
response: {
|
|
34
|
+
"message": "Found 1 file with matches:",
|
|
35
|
+
"results": [
|
|
36
|
+
{
|
|
37
|
+
"filename": "Projects/AI.md",
|
|
38
|
+
"matches": [
|
|
39
|
+
{
|
|
40
|
+
"context": "Research needed:\n#todo Implement transformer architecture\nDeadline: Next week",
|
|
41
|
+
"match": { "start": 15, "end": 45 }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
description: "Example response with many matches (file-only format)",
|
|
50
|
+
args: {
|
|
51
|
+
query: "API"
|
|
52
|
+
},
|
|
53
|
+
response: {
|
|
54
|
+
"message": "Found 92 files with matches. Showing file names only:",
|
|
55
|
+
"results": [
|
|
56
|
+
{
|
|
57
|
+
"filename": "Developer/Documentation/API.md",
|
|
58
|
+
"matchCount": 43
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"filename": "Projects/API_Design.md",
|
|
62
|
+
"matchCount": 34
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
query: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Text pattern to search for. Can include tags, keywords, or phrases."
|
|
74
|
+
},
|
|
75
|
+
contextLength: {
|
|
76
|
+
type: "integer",
|
|
77
|
+
description: "Number of characters to include before and after each match for context (default: 10)",
|
|
78
|
+
default: 10
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
required: ["query"]
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async runTool(args) {
|
|
86
|
+
try {
|
|
87
|
+
logger.debug(`Searching for "${args.query}" with context length: ${args.contextLength || 100}`);
|
|
88
|
+
const results = await this.client.search(args.query, args.contextLength ?? 100);
|
|
89
|
+
// If more than 5 results, only return filenames and match counts to prevent overwhelming responses
|
|
90
|
+
if (results.length > 5) {
|
|
91
|
+
const fileOnlyResults = results.map(result => ({
|
|
92
|
+
filename: result.filename,
|
|
93
|
+
matchCount: result.matches.length
|
|
94
|
+
}));
|
|
95
|
+
logger.debug(`Found ${results.length} files with matches, returning file-only format`);
|
|
96
|
+
return this.createResponse({
|
|
97
|
+
message: `Found ${results.length} files with matches. Showing file names only:`,
|
|
98
|
+
results: fileOnlyResults
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Otherwise return full context as before
|
|
102
|
+
const formattedResults = results.map(result => ({
|
|
103
|
+
filename: result.filename,
|
|
104
|
+
matches: result.matches.map(match => ({
|
|
105
|
+
context: match.context,
|
|
106
|
+
match: {
|
|
107
|
+
text: match.context.substring(match.match.start, match.match.end),
|
|
108
|
+
position: {
|
|
109
|
+
start: match.match.start,
|
|
110
|
+
end: match.match.end
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})),
|
|
114
|
+
score: result.score
|
|
115
|
+
}));
|
|
116
|
+
logger.debug(`Found ${results.length} files with matches, returning with context`);
|
|
117
|
+
return this.createResponse({
|
|
118
|
+
message: `Found ${results.length} file(s) with matches:`,
|
|
119
|
+
results: formattedResults
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return this.handleError(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=simple.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling utilities for the Obsidian MCP Server
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Error class for Obsidian MCP Server specific errors
|
|
6
|
+
*/
|
|
7
|
+
export class ObsidianError extends Error {
|
|
8
|
+
details;
|
|
9
|
+
errorCode;
|
|
10
|
+
constructor(message, errorCode = 50000, // Default server error code
|
|
11
|
+
details) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.details = details;
|
|
14
|
+
this.name = "ObsidianError";
|
|
15
|
+
// Ensure 5-digit error code
|
|
16
|
+
if (errorCode < 10000 || errorCode > 99999) {
|
|
17
|
+
// Convert HTTP status codes to 5-digit codes
|
|
18
|
+
// 4xx -> 4xxxx
|
|
19
|
+
// 5xx -> 5xxxx
|
|
20
|
+
this.errorCode = errorCode < 1000 ? errorCode * 100 : 50000;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
this.errorCode = errorCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Convert to API error format
|
|
27
|
+
toApiError() {
|
|
28
|
+
return {
|
|
29
|
+
errorCode: this.errorCode,
|
|
30
|
+
message: this.message
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Maps HTTP status codes to internal error codes
|
|
36
|
+
*/
|
|
37
|
+
export function getErrorCodeFromStatus(status) {
|
|
38
|
+
switch (status) {
|
|
39
|
+
case 400: return 40000; // Bad request
|
|
40
|
+
case 401: return 40100; // Unauthorized
|
|
41
|
+
case 403: return 40300; // Forbidden
|
|
42
|
+
case 404: return 40400; // Not found
|
|
43
|
+
case 405: return 40500; // Method not allowed
|
|
44
|
+
case 409: return 40900; // Conflict
|
|
45
|
+
case 429: return 42900; // Too many requests
|
|
46
|
+
case 500: return 50000; // Internal server error
|
|
47
|
+
case 501: return 50100; // Not implemented
|
|
48
|
+
case 502: return 50200; // Bad gateway
|
|
49
|
+
case 503: return 50300; // Service unavailable
|
|
50
|
+
case 504: return 50400; // Gateway timeout
|
|
51
|
+
default:
|
|
52
|
+
if (status >= 400 && status < 500)
|
|
53
|
+
return 40000 + (status - 400) * 100;
|
|
54
|
+
if (status >= 500 && status < 600)
|
|
55
|
+
return 50000 + (status - 500) * 100;
|
|
56
|
+
return 50000;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities for the Obsidian MCP Server
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Log levels
|
|
6
|
+
*/
|
|
7
|
+
export var LogLevel;
|
|
8
|
+
(function (LogLevel) {
|
|
9
|
+
LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
|
|
10
|
+
LogLevel[LogLevel["WARN"] = 1] = "WARN";
|
|
11
|
+
LogLevel[LogLevel["INFO"] = 2] = "INFO";
|
|
12
|
+
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
|
|
13
|
+
LogLevel[LogLevel["TRACE"] = 4] = "TRACE";
|
|
14
|
+
})(LogLevel || (LogLevel = {}));
|
|
15
|
+
/**
|
|
16
|
+
* Default logger configuration
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_CONFIG = {
|
|
19
|
+
level: process.env.NODE_ENV === 'production'
|
|
20
|
+
? LogLevel.INFO
|
|
21
|
+
: LogLevel.DEBUG,
|
|
22
|
+
includeTimestamps: true,
|
|
23
|
+
includeLevel: true
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Simple logger for MCP server operations
|
|
27
|
+
*/
|
|
28
|
+
export class Logger {
|
|
29
|
+
name;
|
|
30
|
+
config;
|
|
31
|
+
constructor(name, config = {}) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Internal method to format and output a log message
|
|
37
|
+
*/
|
|
38
|
+
log(level, message, ...args) {
|
|
39
|
+
if (level > this.config.level)
|
|
40
|
+
return;
|
|
41
|
+
const parts = [];
|
|
42
|
+
// Add timestamp if configured
|
|
43
|
+
if (this.config.includeTimestamps) {
|
|
44
|
+
parts.push(`[${new Date().toISOString()}]`);
|
|
45
|
+
}
|
|
46
|
+
// Add level if configured
|
|
47
|
+
if (this.config.includeLevel) {
|
|
48
|
+
const levelStr = LogLevel[level] || 'UNKNOWN';
|
|
49
|
+
parts.push(`[${levelStr}]`);
|
|
50
|
+
}
|
|
51
|
+
// Add component name
|
|
52
|
+
parts.push(`[${this.name}]`);
|
|
53
|
+
// Add message
|
|
54
|
+
parts.push(message);
|
|
55
|
+
// Output to console
|
|
56
|
+
const output = parts.join(' ');
|
|
57
|
+
switch (level) {
|
|
58
|
+
case LogLevel.ERROR:
|
|
59
|
+
console.error(output, ...args);
|
|
60
|
+
break;
|
|
61
|
+
case LogLevel.WARN:
|
|
62
|
+
console.warn(output, ...args);
|
|
63
|
+
break;
|
|
64
|
+
case LogLevel.INFO:
|
|
65
|
+
console.info(output, ...args);
|
|
66
|
+
break;
|
|
67
|
+
case LogLevel.DEBUG:
|
|
68
|
+
case LogLevel.TRACE:
|
|
69
|
+
default:
|
|
70
|
+
console.debug(output, ...args);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Log an error message
|
|
76
|
+
*/
|
|
77
|
+
error(message, ...args) {
|
|
78
|
+
this.log(LogLevel.ERROR, message, ...args);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Log a warning message
|
|
82
|
+
*/
|
|
83
|
+
warn(message, ...args) {
|
|
84
|
+
this.log(LogLevel.WARN, message, ...args);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Log an info message
|
|
88
|
+
*/
|
|
89
|
+
info(message, ...args) {
|
|
90
|
+
this.log(LogLevel.INFO, message, ...args);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Log a debug message
|
|
94
|
+
*/
|
|
95
|
+
debug(message, ...args) {
|
|
96
|
+
this.log(LogLevel.DEBUG, message, ...args);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Log a trace message
|
|
100
|
+
*/
|
|
101
|
+
trace(message, ...args) {
|
|
102
|
+
this.log(LogLevel.TRACE, message, ...args);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Set the log level
|
|
106
|
+
*/
|
|
107
|
+
setLevel(level) {
|
|
108
|
+
this.config.level = level;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create and return a namespaced logger
|
|
113
|
+
*/
|
|
114
|
+
export function createLogger(name, config) {
|
|
115
|
+
return new Logger(name, config);
|
|
116
|
+
}
|
|
117
|
+
// Create a global root logger
|
|
118
|
+
export const rootLogger = createLogger('mcp-server');
|
|
119
|
+
//# sourceMappingURL=logging.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiting utilities for the Obsidian MCP Server
|
|
3
|
+
*/
|
|
4
|
+
import { ObsidianError } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Default rate limit configuration
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_RATE_LIMIT_CONFIG = {
|
|
9
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
10
|
+
maxRequests: 200
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* RateLimit manages request rate limiting for API endpoints
|
|
14
|
+
*/
|
|
15
|
+
export class RateLimiter {
|
|
16
|
+
config;
|
|
17
|
+
requestCounts = new Map();
|
|
18
|
+
cleanupInterval;
|
|
19
|
+
constructor(config = DEFAULT_RATE_LIMIT_CONFIG) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
// Clean up expired rate limit entries periodically
|
|
22
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000); // Clean up every minute
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if a request is within rate limits
|
|
26
|
+
* @param key Identifier for the rate limit bucket (e.g., toolName)
|
|
27
|
+
* @returns Whether the request is allowed
|
|
28
|
+
*/
|
|
29
|
+
checkRateLimit(key) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const requestInfo = this.requestCounts.get(key);
|
|
32
|
+
if (!requestInfo || now > requestInfo.resetTime) {
|
|
33
|
+
// Reset counter for new window
|
|
34
|
+
this.requestCounts.set(key, {
|
|
35
|
+
count: 1,
|
|
36
|
+
resetTime: now + this.config.windowMs
|
|
37
|
+
});
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (requestInfo.count >= this.config.maxRequests) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
requestInfo.count++;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check rate limit and throw an error if exceeded
|
|
48
|
+
* @param key Identifier for the rate limit bucket
|
|
49
|
+
* @throws ObsidianError if rate limit is exceeded
|
|
50
|
+
*/
|
|
51
|
+
enforceRateLimit(key) {
|
|
52
|
+
if (!this.checkRateLimit(key)) {
|
|
53
|
+
throw new ObsidianError(`Rate limit exceeded for ${key}. Please try again later.`, 42900 // 42900 = Rate limit exceeded
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get information about current rate limit status
|
|
59
|
+
* @param key Identifier for the rate limit bucket
|
|
60
|
+
* @returns Rate limit information or null if no requests have been made
|
|
61
|
+
*/
|
|
62
|
+
getRateLimitInfo(key) {
|
|
63
|
+
const requestInfo = this.requestCounts.get(key);
|
|
64
|
+
if (!requestInfo) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
remaining: Math.max(0, this.config.maxRequests - requestInfo.count),
|
|
69
|
+
resetTime: requestInfo.resetTime
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Clean up expired rate limit entries
|
|
74
|
+
*/
|
|
75
|
+
cleanup() {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
for (const [key, info] of this.requestCounts.entries()) {
|
|
78
|
+
if (now > info.resetTime) {
|
|
79
|
+
this.requestCounts.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clean up resources (e.g., when shutting down)
|
|
85
|
+
*/
|
|
86
|
+
dispose() {
|
|
87
|
+
if (this.cleanupInterval) {
|
|
88
|
+
clearInterval(this.cleanupInterval);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Export a singleton instance with default configuration
|
|
93
|
+
export const rateLimiter = new RateLimiter();
|
|
94
|
+
//# sourceMappingURL=rate-limiting.js.map
|