mcp-server-logseq 0.0.1__py3-none-any.whl
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.
- mcp_server_logseq/__init__.py +43 -0
- mcp_server_logseq/__main__.py +5 -0
- mcp_server_logseq/server.py +833 -0
- mcp_server_logseq-0.0.1.dist-info/METADATA +124 -0
- mcp_server_logseq-0.0.1.dist-info/RECORD +8 -0
- mcp_server_logseq-0.0.1.dist-info/WHEEL +4 -0
- mcp_server_logseq-0.0.1.dist-info/entry_points.txt +2 -0
- mcp_server_logseq-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .server import serve
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
"""MCP LogSeq Server - AI-powered note taking for MCP"""
|
|
6
|
+
import argparse
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="Share your LogSeq notes with LLM (https://docs.logseq.com/#/page/local%20http%20server)"
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--api-key",
|
|
18
|
+
type=str,
|
|
19
|
+
help="LogSeq API key",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--url",
|
|
23
|
+
type=str,
|
|
24
|
+
help="LogSeq API host",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
# Check for API key in args first, then environment
|
|
30
|
+
api_key = args.api_key or os.getenv("LOGSEQ_API_TOKEN")
|
|
31
|
+
if not api_key:
|
|
32
|
+
parser.error("LogSeq API key must be provided either via --api-key or LOGSEQ_API_TOKEN environment variable")
|
|
33
|
+
|
|
34
|
+
# Check for URL in args first, then environment
|
|
35
|
+
url = args.url or os.getenv("LOGSEQ_API_URL")
|
|
36
|
+
if not url:
|
|
37
|
+
parser.error("LogSeq API URL must be provided either via --url or LOGSEQ_API_URL environment variable")
|
|
38
|
+
|
|
39
|
+
asyncio.run(serve(api_key, url))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
from typing import Annotated, Optional
|
|
2
|
+
from mcp.server import Server
|
|
3
|
+
from mcp.shared.exceptions import McpError
|
|
4
|
+
from mcp.types import ErrorData
|
|
5
|
+
from mcp.server.stdio import stdio_server
|
|
6
|
+
from mcp.types import (
|
|
7
|
+
GetPromptResult,
|
|
8
|
+
Prompt,
|
|
9
|
+
PromptArgument,
|
|
10
|
+
PromptMessage,
|
|
11
|
+
TextContent,
|
|
12
|
+
Tool,
|
|
13
|
+
INVALID_PARAMS,
|
|
14
|
+
INTERNAL_ERROR,
|
|
15
|
+
)
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
17
|
+
import requests
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LogseqBaseModel(BaseModel):
|
|
22
|
+
"""Base model with Pydantic configuration"""
|
|
23
|
+
model_config = ConfigDict(extra='forbid', validate_assignment=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InsertBlockParams(LogseqBaseModel):
|
|
27
|
+
"""Parameters for inserting a new block in Logseq."""
|
|
28
|
+
parent_block: Annotated[
|
|
29
|
+
Optional[str],
|
|
30
|
+
Field(default=None, description="UUID or content of parent block")
|
|
31
|
+
]
|
|
32
|
+
content: Annotated[
|
|
33
|
+
str,
|
|
34
|
+
Field(description="Content of the new block")
|
|
35
|
+
]
|
|
36
|
+
is_page_block: Annotated[
|
|
37
|
+
Optional[bool],
|
|
38
|
+
Field(default=False, description="Page-level block flag")
|
|
39
|
+
]
|
|
40
|
+
before: Annotated[
|
|
41
|
+
Optional[bool],
|
|
42
|
+
Field(default=False, description="Insert before parent")
|
|
43
|
+
]
|
|
44
|
+
custom_uuid: Annotated[
|
|
45
|
+
Optional[str],
|
|
46
|
+
Field(default=None, description="Custom UUID for block")
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
@field_validator('parent_block', 'custom_uuid', mode='before')
|
|
50
|
+
@classmethod
|
|
51
|
+
def validate_block_references(cls, value):
|
|
52
|
+
"""Validate block/page references"""
|
|
53
|
+
if value and isinstance(value, str):
|
|
54
|
+
if value.startswith('((') and value.endswith('))'):
|
|
55
|
+
return value.strip('()')
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CreatePageParams(LogseqBaseModel):
|
|
60
|
+
"""Parameters for creating a new page in Logseq."""
|
|
61
|
+
page_name: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
Field(description="Name of the page to create")
|
|
64
|
+
]
|
|
65
|
+
properties: Annotated[
|
|
66
|
+
Optional[dict],
|
|
67
|
+
Field(default=None, description="Page properties")
|
|
68
|
+
]
|
|
69
|
+
journal: Annotated[
|
|
70
|
+
Optional[bool],
|
|
71
|
+
Field(default=False, description="Journal page flag")
|
|
72
|
+
]
|
|
73
|
+
format: Annotated[
|
|
74
|
+
Optional[str],
|
|
75
|
+
Field(default="markdown", description="Page format")
|
|
76
|
+
]
|
|
77
|
+
create_first_block: Annotated[
|
|
78
|
+
Optional[bool],
|
|
79
|
+
Field(default=True, description="Create initial block")
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
@field_validator('properties', mode='before')
|
|
83
|
+
@classmethod
|
|
84
|
+
def parse_properties(cls, value):
|
|
85
|
+
"""Parse properties from JSON string if needed"""
|
|
86
|
+
if isinstance(value, str):
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(value)
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
raise ValueError("Invalid JSON format for properties")
|
|
91
|
+
return value or {}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class GetCurrentPageParams(LogseqBaseModel):
|
|
95
|
+
"""Parameters for getting current page (no arguments needed)"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class GetPageParams(LogseqBaseModel):
|
|
99
|
+
"""Parameters for retrieving a specific page"""
|
|
100
|
+
src_page: Annotated[
|
|
101
|
+
str | int,
|
|
102
|
+
Field(
|
|
103
|
+
description="Page identifier (name, UUID or database ID)",
|
|
104
|
+
examples=["[[Journal/2024-03-15]]", 12345]
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
include_children: Annotated[
|
|
108
|
+
Optional[bool],
|
|
109
|
+
Field(
|
|
110
|
+
default=False,
|
|
111
|
+
description="Include child blocks in response"
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class GetAllPagesParams(LogseqBaseModel):
|
|
117
|
+
"""Parameters for listing all pages"""
|
|
118
|
+
repo: Annotated[
|
|
119
|
+
Optional[str],
|
|
120
|
+
Field(
|
|
121
|
+
default=None,
|
|
122
|
+
description="Repository name (default: current graph)"
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EditBlockParams(LogseqBaseModel):
|
|
128
|
+
src_block: Annotated[
|
|
129
|
+
str,
|
|
130
|
+
Field(description="Block UUID or reference", examples=["6485a-9de3...", "[[Page/Block]]"])
|
|
131
|
+
]
|
|
132
|
+
pos: Annotated[
|
|
133
|
+
int,
|
|
134
|
+
Field(
|
|
135
|
+
default=0,
|
|
136
|
+
description="Cursor position in block content",
|
|
137
|
+
ge=0,
|
|
138
|
+
le=10000
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ExitEditingModeParams(LogseqBaseModel):
|
|
144
|
+
select_block: Annotated[
|
|
145
|
+
bool,
|
|
146
|
+
Field(
|
|
147
|
+
default=False,
|
|
148
|
+
description="Keep block selected after exiting edit mode"
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GetPageBlocksTreeParams(LogseqBaseModel):
|
|
154
|
+
src_page: Annotated[
|
|
155
|
+
str,
|
|
156
|
+
Field(description="Page name or UUID", examples=["[[Journal]]", "6485a-9de3..."])
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class EmptyParams(LogseqBaseModel):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
class GetEditingBlockContentParams(LogseqBaseModel):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
class GetCurrentBlocksTreeParams(LogseqBaseModel):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def serve(
|
|
171
|
+
api_key: str,
|
|
172
|
+
logseq_url: str = "http://localhost:12315"
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Run the Logseq MCP server.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
api_key: Logseq API token for authentication
|
|
178
|
+
logseq_url: Base URL of Logseq graph (default: http://localhost:12315)
|
|
179
|
+
"""
|
|
180
|
+
server = Server("mcp-sever-logseq")
|
|
181
|
+
|
|
182
|
+
def make_request(method: str, args: list) -> dict:
|
|
183
|
+
"""Make authenticated request to Logseq API."""
|
|
184
|
+
headers = {
|
|
185
|
+
'Authorization': f'Bearer {api_key}',
|
|
186
|
+
'Content-Type': 'application/json'
|
|
187
|
+
}
|
|
188
|
+
payload = {"method": method, "args": args}
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
response = requests.post(
|
|
192
|
+
f"{logseq_url}/api",
|
|
193
|
+
headers=headers,
|
|
194
|
+
json=payload,
|
|
195
|
+
timeout=10
|
|
196
|
+
)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
return response.json()
|
|
199
|
+
except requests.exceptions.HTTPError as e:
|
|
200
|
+
if response.status_code == 401:
|
|
201
|
+
raise McpError(ErrorData(INTERNAL_ERROR, "Invalid API token"))
|
|
202
|
+
raise McpError(ErrorData(INTERNAL_ERROR, f"API request failed: {str(e)}"))
|
|
203
|
+
except requests.exceptions.RequestException as e:
|
|
204
|
+
raise McpError(ErrorData(INTERNAL_ERROR, f"Network error: {str(e)}"))
|
|
205
|
+
|
|
206
|
+
@server.list_tools()
|
|
207
|
+
async def list_tools() -> list[Tool]:
|
|
208
|
+
return [
|
|
209
|
+
Tool(
|
|
210
|
+
name="logseq_insert_block",
|
|
211
|
+
description="""Insert a new block into Logseq. Can create:
|
|
212
|
+
- Page-level blocks (use is_page_block=true with page name as parent_block)
|
|
213
|
+
- Nested blocks under existing blocks
|
|
214
|
+
- Blocks with custom UUIDs for precise reference
|
|
215
|
+
Supports before/after positioning and property management.""",
|
|
216
|
+
inputSchema=InsertBlockParams.model_json_schema(),
|
|
217
|
+
),
|
|
218
|
+
Tool(
|
|
219
|
+
name="logseq_create_page",
|
|
220
|
+
description="""Create a new page in Logseq with optional properties.
|
|
221
|
+
Features:
|
|
222
|
+
- Journal page creation with date formatting
|
|
223
|
+
- Custom page properties (tags, status, etc.)
|
|
224
|
+
- Format selection (Markdown/Org-mode)
|
|
225
|
+
- Automatic first block creation
|
|
226
|
+
Perfect for template-based page creation and knowledge management.""",
|
|
227
|
+
inputSchema=CreatePageParams.model_json_schema(),
|
|
228
|
+
),
|
|
229
|
+
Tool(
|
|
230
|
+
name="logseq_get_current_page",
|
|
231
|
+
description="Retrieves the currently active page or block in the user's workspace",
|
|
232
|
+
inputSchema=GetCurrentPageParams.model_json_schema(),
|
|
233
|
+
),
|
|
234
|
+
Tool(
|
|
235
|
+
name="logseq_get_page",
|
|
236
|
+
description="Retrieve detailed information about a specific page including metadata and content",
|
|
237
|
+
inputSchema=GetPageParams.model_json_schema(),
|
|
238
|
+
),
|
|
239
|
+
Tool(
|
|
240
|
+
name="logseq_get_all_pages",
|
|
241
|
+
description="List all pages in the graph with basic metadata",
|
|
242
|
+
inputSchema=GetAllPagesParams.model_json_schema(),
|
|
243
|
+
),
|
|
244
|
+
Tool(
|
|
245
|
+
name="logseq_edit_block",
|
|
246
|
+
description="Enter editing mode for a specific block",
|
|
247
|
+
inputSchema=EditBlockParams.model_json_schema(),
|
|
248
|
+
),
|
|
249
|
+
Tool(
|
|
250
|
+
name="logseq_exit_editing_mode",
|
|
251
|
+
description="Exit current editing mode",
|
|
252
|
+
inputSchema=ExitEditingModeParams.model_json_schema(),
|
|
253
|
+
),
|
|
254
|
+
Tool(
|
|
255
|
+
name="logseq_get_current_page_content",
|
|
256
|
+
description="Get hierarchical block structure of current page",
|
|
257
|
+
inputSchema=GetCurrentBlocksTreeParams.model_json_schema() # No parameters
|
|
258
|
+
),
|
|
259
|
+
Tool(
|
|
260
|
+
name="logseq_get_editing_block_content",
|
|
261
|
+
description="Get content of currently edited block",
|
|
262
|
+
inputSchema=GetEditingBlockContentParams.model_json_schema() # No parameters
|
|
263
|
+
),
|
|
264
|
+
Tool(
|
|
265
|
+
name="logseq_get_page_content",
|
|
266
|
+
description="Get block hierarchy for specific page",
|
|
267
|
+
inputSchema=GetPageBlocksTreeParams.model_json_schema(),
|
|
268
|
+
),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
@server.list_prompts()
|
|
272
|
+
async def list_prompts() -> list[Prompt]:
|
|
273
|
+
return [
|
|
274
|
+
Prompt(
|
|
275
|
+
name="logseq_insert_block",
|
|
276
|
+
description="Create a new block in Logseq",
|
|
277
|
+
arguments=[
|
|
278
|
+
PromptArgument(
|
|
279
|
+
name="parent_block",
|
|
280
|
+
description="Parent block UUID or page name (for page blocks)",
|
|
281
|
+
required=False,
|
|
282
|
+
),
|
|
283
|
+
PromptArgument(
|
|
284
|
+
name="content",
|
|
285
|
+
description="Block content in Markdown/Org syntax",
|
|
286
|
+
required=True,
|
|
287
|
+
),
|
|
288
|
+
PromptArgument(
|
|
289
|
+
name="is_page_block",
|
|
290
|
+
description="Set true for page-level blocks",
|
|
291
|
+
required=False,
|
|
292
|
+
),
|
|
293
|
+
],
|
|
294
|
+
),
|
|
295
|
+
Prompt(
|
|
296
|
+
name="logseq_create_page",
|
|
297
|
+
description="Create a new Logseq page",
|
|
298
|
+
arguments=[
|
|
299
|
+
PromptArgument(
|
|
300
|
+
name="page_name",
|
|
301
|
+
description="Name of the page to create",
|
|
302
|
+
required=True,
|
|
303
|
+
),
|
|
304
|
+
PromptArgument(
|
|
305
|
+
name="properties",
|
|
306
|
+
description="Optional page properties as JSON",
|
|
307
|
+
required=False,
|
|
308
|
+
),
|
|
309
|
+
PromptArgument(
|
|
310
|
+
name="journal",
|
|
311
|
+
description="Set true for journal pages",
|
|
312
|
+
required=False,
|
|
313
|
+
),
|
|
314
|
+
],
|
|
315
|
+
),
|
|
316
|
+
Prompt(
|
|
317
|
+
name="logseq_get_current_page",
|
|
318
|
+
description="Get the currently active page or block",
|
|
319
|
+
arguments=[]
|
|
320
|
+
),
|
|
321
|
+
Prompt(
|
|
322
|
+
name="logseq_get_page",
|
|
323
|
+
description="Retrieve information about a specific page",
|
|
324
|
+
arguments=[
|
|
325
|
+
PromptArgument(
|
|
326
|
+
name="src_page",
|
|
327
|
+
description="Page name, UUID or database ID",
|
|
328
|
+
required=True
|
|
329
|
+
)
|
|
330
|
+
]
|
|
331
|
+
),
|
|
332
|
+
Prompt(
|
|
333
|
+
name="logseq_get_all_pages",
|
|
334
|
+
description="List all pages in the graph",
|
|
335
|
+
arguments=[
|
|
336
|
+
PromptArgument(
|
|
337
|
+
name="repo",
|
|
338
|
+
description="Repository name (optional)",
|
|
339
|
+
required=False
|
|
340
|
+
)
|
|
341
|
+
]
|
|
342
|
+
),
|
|
343
|
+
Prompt(
|
|
344
|
+
name="logseq_edit_block",
|
|
345
|
+
description="Edit specific block content",
|
|
346
|
+
arguments=[
|
|
347
|
+
PromptArgument(
|
|
348
|
+
name="src_block",
|
|
349
|
+
description="Block identifier",
|
|
350
|
+
required=True
|
|
351
|
+
)
|
|
352
|
+
]
|
|
353
|
+
),
|
|
354
|
+
Prompt(
|
|
355
|
+
name="logseq_exit_editing_mode",
|
|
356
|
+
description="Exit block editing mode",
|
|
357
|
+
arguments=[
|
|
358
|
+
PromptArgument(
|
|
359
|
+
name="select_block",
|
|
360
|
+
description="Keep block selected",
|
|
361
|
+
required=False
|
|
362
|
+
)
|
|
363
|
+
]
|
|
364
|
+
),
|
|
365
|
+
Prompt(
|
|
366
|
+
name="logseq_get_current_page_content",
|
|
367
|
+
description="Get current page's content by each block",
|
|
368
|
+
arguments=[]
|
|
369
|
+
),
|
|
370
|
+
Prompt(
|
|
371
|
+
name="logseq_get_editing_block_content",
|
|
372
|
+
description="Get content of active editing block",
|
|
373
|
+
arguments=[]
|
|
374
|
+
),
|
|
375
|
+
Prompt(
|
|
376
|
+
name="logseq_get_page_content",
|
|
377
|
+
description="Get block page content by each block",
|
|
378
|
+
arguments=[
|
|
379
|
+
PromptArgument(
|
|
380
|
+
name="src_page",
|
|
381
|
+
description="Page identifier",
|
|
382
|
+
required=True
|
|
383
|
+
)
|
|
384
|
+
]
|
|
385
|
+
),
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
def format_block_result(result: dict) -> str:
|
|
389
|
+
"""Format block creation result into readable text."""
|
|
390
|
+
return (
|
|
391
|
+
f"Created block in {result.get('page', {}).get('name', 'unknown page')}\n"
|
|
392
|
+
f"UUID: {result.get('uuid')}\n"
|
|
393
|
+
f"Content: {result.get('content')}\n"
|
|
394
|
+
f"Parent: {result.get('parent', {}).get('uuid') or 'None'}"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def format_page_result(result: dict) -> str:
|
|
398
|
+
"""Format page creation result into readable text."""
|
|
399
|
+
return (
|
|
400
|
+
f"Created page: {result.get('name')}\n"
|
|
401
|
+
f"UUID: {result.get('uuid')}\n"
|
|
402
|
+
f"Journal: {result.get('journal', False)}\n"
|
|
403
|
+
f"Blocks: {len(result.get('blocks', []))}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def format_page_detail(page: dict) -> str:
|
|
407
|
+
"""Format single page details"""
|
|
408
|
+
return (
|
|
409
|
+
f"Page: {page.get('name', 'Unnamed')}\n"
|
|
410
|
+
f"UUID: {page.get('uuid')}\n"
|
|
411
|
+
f"Created: {page.get('createdAt', 0)}\n"
|
|
412
|
+
f"Updated: {page.get('updatedAt', 0)}\n"
|
|
413
|
+
f"Blocks: {len(page.get('blocks', []))}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def format_pages_list(pages: list) -> str:
|
|
417
|
+
"""Format list of pages"""
|
|
418
|
+
return "\n".join(
|
|
419
|
+
f"{p['name']} (UUID: {p['uuid']})"
|
|
420
|
+
for p in sorted(pages, key=lambda x: x.get('name', ''))
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _format_current_page(result: dict) -> str:
|
|
424
|
+
"""Special formatting for current page/block context"""
|
|
425
|
+
entity_type = "Page" if 'name' in result else "Block"
|
|
426
|
+
return (
|
|
427
|
+
f"Current {entity_type}: {result.get('name', result.get('content', 'Untitled'))}\n"
|
|
428
|
+
f"UUID: {result.get('uuid')}\n"
|
|
429
|
+
f"Last updated: {result.get('updatedAt', 'N/A')}"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def format_blocks_tree(blocks: list) -> str:
|
|
433
|
+
"""Format hierarchical block structure"""
|
|
434
|
+
def print_tree(block, level=0):
|
|
435
|
+
output = []
|
|
436
|
+
prefix = " " * level + "- "
|
|
437
|
+
output.append(f"{prefix}{block.get('content', '')}")
|
|
438
|
+
for child in block.get('children', []):
|
|
439
|
+
output.extend(print_tree(child, level + 1))
|
|
440
|
+
return output
|
|
441
|
+
|
|
442
|
+
return "\n".join(
|
|
443
|
+
line for block in blocks
|
|
444
|
+
for line in print_tree(block)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def _format_no_arg_result(method_name: str, result: dict) -> str:
|
|
448
|
+
"""Format results for methods without arguments"""
|
|
449
|
+
formatters = {
|
|
450
|
+
'logseq_get_current_page_content': lambda r: format_blocks_tree(r),
|
|
451
|
+
'logseq_get_editing_block_content': lambda r: f"Current content:\n{r}",
|
|
452
|
+
'logseq_get_current_page': _format_current_page
|
|
453
|
+
}
|
|
454
|
+
return formatters[method_name](result)
|
|
455
|
+
|
|
456
|
+
def format_no_arg_result(name: str, result) -> str:
|
|
457
|
+
"""Format results for methods without arguments"""
|
|
458
|
+
formatters = {
|
|
459
|
+
'logseq_get_current_page': lambda r: (
|
|
460
|
+
f"Current: {r.get('name', r.get('content', 'Untitled'))}\n"
|
|
461
|
+
f"UUID: {r.get('uuid')}\n"
|
|
462
|
+
f"Last updated: {r.get('updatedAt', 'N/A')}"
|
|
463
|
+
),
|
|
464
|
+
'logseq_get_current_page_content': lambda r: format_blocks_tree(r),
|
|
465
|
+
'logseq_get_editing_block_content': lambda r: f"Current content:\n{r}",
|
|
466
|
+
'logseq_get_all_pages': lambda r: "\n".join(
|
|
467
|
+
f"{p['name']} ({p['uuid']})" for p in sorted(r, key=lambda x: x['name'])
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
return formatters[name](result)
|
|
471
|
+
|
|
472
|
+
@server.call_tool()
|
|
473
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
474
|
+
try:
|
|
475
|
+
if name == "logseq_insert_block":
|
|
476
|
+
args = InsertBlockParams(**arguments)
|
|
477
|
+
result = make_request(
|
|
478
|
+
"logseq.Editor.insertBlock",
|
|
479
|
+
[
|
|
480
|
+
args.parent_block,
|
|
481
|
+
args.content,
|
|
482
|
+
{
|
|
483
|
+
"isPageBlock": args.is_page_block,
|
|
484
|
+
"before": args.before,
|
|
485
|
+
"customUUID": args.custom_uuid
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
)
|
|
489
|
+
return [TextContent(
|
|
490
|
+
type="text",
|
|
491
|
+
text=format_block_result(result)
|
|
492
|
+
)]
|
|
493
|
+
|
|
494
|
+
elif name == "logseq_create_page":
|
|
495
|
+
args = CreatePageParams(**arguments)
|
|
496
|
+
result = make_request(
|
|
497
|
+
"logseq.Editor.createPage",
|
|
498
|
+
[
|
|
499
|
+
args.page_name,
|
|
500
|
+
args.properties or {},
|
|
501
|
+
{
|
|
502
|
+
"journal": args.journal,
|
|
503
|
+
"format": args.format,
|
|
504
|
+
"createFirstBlock": args.create_first_block
|
|
505
|
+
}
|
|
506
|
+
]
|
|
507
|
+
)
|
|
508
|
+
return [TextContent(
|
|
509
|
+
type="text",
|
|
510
|
+
text=format_page_result(result)
|
|
511
|
+
)]
|
|
512
|
+
|
|
513
|
+
elif name == "logseq_get_current_page":
|
|
514
|
+
args = GetCurrentPageParams(**arguments)
|
|
515
|
+
result = make_request(
|
|
516
|
+
"logseq.Editor.getCurrentPage",
|
|
517
|
+
[]
|
|
518
|
+
)
|
|
519
|
+
return [TextContent(
|
|
520
|
+
type="text",
|
|
521
|
+
text=format_page_result(result)
|
|
522
|
+
)]
|
|
523
|
+
|
|
524
|
+
elif name == "logseq_get_page":
|
|
525
|
+
args = GetPageParams(**arguments)
|
|
526
|
+
result = make_request(
|
|
527
|
+
"logseq.Editor.getPage",
|
|
528
|
+
[
|
|
529
|
+
args.src_page,
|
|
530
|
+
{"includeChildren": args.include_children}
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
return [TextContent(
|
|
534
|
+
type="text",
|
|
535
|
+
text=format_page_detail(result)
|
|
536
|
+
)]
|
|
537
|
+
|
|
538
|
+
elif name == "logseq_get_all_pages":
|
|
539
|
+
args = GetAllPagesParams(**arguments)
|
|
540
|
+
result = make_request(
|
|
541
|
+
"logseq.Editor.getAllPages",
|
|
542
|
+
[args.repo] if args.repo else []
|
|
543
|
+
)
|
|
544
|
+
return [TextContent(
|
|
545
|
+
type="text",
|
|
546
|
+
text=format_pages_list(result)
|
|
547
|
+
)]
|
|
548
|
+
|
|
549
|
+
elif name == "logseq_edit_block":
|
|
550
|
+
args = EditBlockParams(**arguments)
|
|
551
|
+
result = make_request(
|
|
552
|
+
"logseq.Editor.editBlock",
|
|
553
|
+
[args.src_block, {"pos": args.pos}]
|
|
554
|
+
)
|
|
555
|
+
return [TextContent(
|
|
556
|
+
type="text",
|
|
557
|
+
text=f"Editing block {args.src_block} at position {args.pos}"
|
|
558
|
+
)]
|
|
559
|
+
|
|
560
|
+
elif name == "logseq_exit_editing_mode":
|
|
561
|
+
args = ExitEditingModeParams(**arguments)
|
|
562
|
+
make_request(
|
|
563
|
+
"logseq.Editor.exitEditingMode",
|
|
564
|
+
[args.select_block]
|
|
565
|
+
)
|
|
566
|
+
return [TextContent(
|
|
567
|
+
type="text",
|
|
568
|
+
text="Exited editing mode" +
|
|
569
|
+
(" with block selected" if args.select_block else "")
|
|
570
|
+
)]
|
|
571
|
+
|
|
572
|
+
elif name == "logseq_get_current_page_content":
|
|
573
|
+
result = make_request("logseq.Editor.getCurrentPageBlocksTree", [])
|
|
574
|
+
return [TextContent(
|
|
575
|
+
type="text",
|
|
576
|
+
text=format_blocks_tree(result)
|
|
577
|
+
)]
|
|
578
|
+
|
|
579
|
+
elif name == "logseq_get_editing_block_content":
|
|
580
|
+
result = make_request("logseq.Editor.getEditingBlockContent", [])
|
|
581
|
+
return [TextContent(
|
|
582
|
+
type="text",
|
|
583
|
+
text=f"Current editing block content:\n{result}"
|
|
584
|
+
)]
|
|
585
|
+
|
|
586
|
+
elif name == "logseq_get_page_content":
|
|
587
|
+
args = GetPageBlocksTreeParams(**arguments)
|
|
588
|
+
result = make_request(
|
|
589
|
+
"logseq.Editor.getPageBlocksTree",
|
|
590
|
+
[args.src_page]
|
|
591
|
+
)
|
|
592
|
+
return [TextContent(
|
|
593
|
+
type="text",
|
|
594
|
+
text=format_blocks_tree(result)
|
|
595
|
+
)]
|
|
596
|
+
|
|
597
|
+
else:
|
|
598
|
+
raise McpError(ErrorData(INVALID_PARAMS, f"Unknown tool: {name}"))
|
|
599
|
+
|
|
600
|
+
except ValueError as e:
|
|
601
|
+
raise McpError(ErrorData(INVALID_PARAMS, str(e)))
|
|
602
|
+
|
|
603
|
+
@server.get_prompt()
|
|
604
|
+
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
|
|
605
|
+
try:
|
|
606
|
+
# Handle methods that don't require arguments
|
|
607
|
+
no_arg_methods = {
|
|
608
|
+
'logseq_get_current_page',
|
|
609
|
+
'logseq_get_current_page_content',
|
|
610
|
+
'logseq_get_editing_block_content',
|
|
611
|
+
'logseq_get_all_pages'
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
# Normalize arguments
|
|
615
|
+
if arguments is None:
|
|
616
|
+
arguments = {}
|
|
617
|
+
|
|
618
|
+
# Automatic handling for no-argument methods
|
|
619
|
+
if name in no_arg_methods and not arguments:
|
|
620
|
+
api_method = name.split('_', 1)[1].replace('_', '.')
|
|
621
|
+
result = make_request(f"logseq.Editor.{api_method}", [])
|
|
622
|
+
return GetPromptResult(
|
|
623
|
+
description=f"Current {name.split('_')[-1].replace('_', ' ')}",
|
|
624
|
+
messages=[
|
|
625
|
+
PromptMessage(
|
|
626
|
+
role="user",
|
|
627
|
+
content=TextContent(
|
|
628
|
+
type="text",
|
|
629
|
+
text=format_no_arg_result(name, result)
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
]
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Handle methods with arguments
|
|
636
|
+
if name == "logseq_insert_block":
|
|
637
|
+
required_args = ["content"]
|
|
638
|
+
if not all(k in arguments for k in required_args):
|
|
639
|
+
raise ValueError(f"Missing required arguments: {required_args}")
|
|
640
|
+
|
|
641
|
+
result = make_request(
|
|
642
|
+
"logseq.Editor.insertBlock",
|
|
643
|
+
[
|
|
644
|
+
arguments.get("parent_block"),
|
|
645
|
+
arguments["content"],
|
|
646
|
+
{
|
|
647
|
+
"isPageBlock": arguments.get("is_page_block", False),
|
|
648
|
+
"before": arguments.get("before", False),
|
|
649
|
+
"customUUID": arguments.get("custom_uuid")
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
)
|
|
653
|
+
return GetPromptResult(
|
|
654
|
+
description=f"Created block: {arguments['content'][:50]}...",
|
|
655
|
+
messages=[
|
|
656
|
+
PromptMessage(
|
|
657
|
+
role="user",
|
|
658
|
+
content=TextContent(
|
|
659
|
+
type="text",
|
|
660
|
+
text=format_block_result(result)
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
]
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
elif name == "logseq_create_page":
|
|
667
|
+
if "page_name" not in arguments:
|
|
668
|
+
raise ValueError("page_name is required")
|
|
669
|
+
|
|
670
|
+
result = make_request(
|
|
671
|
+
"logseq.Editor.createPage",
|
|
672
|
+
[
|
|
673
|
+
arguments["page_name"],
|
|
674
|
+
arguments.get("properties", {}),
|
|
675
|
+
{
|
|
676
|
+
"journal": arguments.get("journal", False),
|
|
677
|
+
"format": arguments.get("format", "markdown"),
|
|
678
|
+
"createFirstBlock": arguments.get("create_first_block", True),
|
|
679
|
+
"redirect": arguments.get("redirect", False)
|
|
680
|
+
}
|
|
681
|
+
]
|
|
682
|
+
)
|
|
683
|
+
return GetPromptResult(
|
|
684
|
+
description=f"Created page: {arguments['page_name']}",
|
|
685
|
+
messages=[
|
|
686
|
+
PromptMessage(
|
|
687
|
+
role="user",
|
|
688
|
+
content=TextContent(
|
|
689
|
+
type="text",
|
|
690
|
+
text=format_page_result(result)
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
]
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
elif name == "logseq_get_page":
|
|
697
|
+
if "src_page" not in arguments:
|
|
698
|
+
raise ValueError("src_page is required")
|
|
699
|
+
|
|
700
|
+
result = make_request(
|
|
701
|
+
"logseq.Editor.getPage",
|
|
702
|
+
[
|
|
703
|
+
arguments["src_page"],
|
|
704
|
+
{"includeChildren": arguments.get("include_children", False)}
|
|
705
|
+
]
|
|
706
|
+
)
|
|
707
|
+
return GetPromptResult(
|
|
708
|
+
description=f"Details for {arguments['src_page']}",
|
|
709
|
+
messages=[
|
|
710
|
+
PromptMessage(
|
|
711
|
+
role="user",
|
|
712
|
+
content=TextContent(
|
|
713
|
+
type="text",
|
|
714
|
+
text=format_page_detail(result)
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
]
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
elif name == "logseq_edit_block":
|
|
721
|
+
if "src_block" not in arguments:
|
|
722
|
+
raise ValueError("src_block is required")
|
|
723
|
+
|
|
724
|
+
pos = arguments.get("pos", 0)
|
|
725
|
+
make_request(
|
|
726
|
+
"logseq.Editor.editBlock",
|
|
727
|
+
[arguments["src_block"], {"pos": pos}]
|
|
728
|
+
)
|
|
729
|
+
return GetPromptResult(
|
|
730
|
+
description=f"Editing block {arguments['src_block']}",
|
|
731
|
+
messages=[
|
|
732
|
+
PromptMessage(
|
|
733
|
+
role="user",
|
|
734
|
+
content=TextContent(
|
|
735
|
+
type="text",
|
|
736
|
+
text=f"Editing mode activated at position {pos}"
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
]
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
elif name == "logseq_exit_editing_mode":
|
|
743
|
+
select_block = arguments.get("select_block", False)
|
|
744
|
+
make_request("logseq.Editor.exitEditingMode", [select_block])
|
|
745
|
+
return GetPromptResult(
|
|
746
|
+
description="Exited editing mode",
|
|
747
|
+
messages=[
|
|
748
|
+
PromptMessage(
|
|
749
|
+
role="user",
|
|
750
|
+
content=TextContent(
|
|
751
|
+
type="text",
|
|
752
|
+
text="Exited editing" +
|
|
753
|
+
(" with block selected" if select_block else "")
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
]
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
elif name == "logseq_get_page_content":
|
|
760
|
+
if "src_page" not in arguments:
|
|
761
|
+
raise ValueError("src_page is required")
|
|
762
|
+
|
|
763
|
+
result = make_request(
|
|
764
|
+
"logseq.Editor.getPageBlocksTree",
|
|
765
|
+
[arguments["src_page"]]
|
|
766
|
+
)
|
|
767
|
+
return GetPromptResult(
|
|
768
|
+
description=f"Block structure for {arguments['src_page']}",
|
|
769
|
+
messages=[
|
|
770
|
+
PromptMessage(
|
|
771
|
+
role="user",
|
|
772
|
+
content=TextContent(
|
|
773
|
+
type="text",
|
|
774
|
+
text=format_blocks_tree(result)
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
]
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
elif name == "logseq_get_all_pages":
|
|
781
|
+
repo = arguments.get("repo")
|
|
782
|
+
result = make_request(
|
|
783
|
+
"logseq.Editor.getAllPages",
|
|
784
|
+
[repo] if repo else []
|
|
785
|
+
)
|
|
786
|
+
return GetPromptResult(
|
|
787
|
+
description=f"All pages in {repo or 'current graph'}",
|
|
788
|
+
messages=[
|
|
789
|
+
PromptMessage(
|
|
790
|
+
role="user",
|
|
791
|
+
content=TextContent(
|
|
792
|
+
type="text",
|
|
793
|
+
text=format_pages_list(result)
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
]
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
else:
|
|
800
|
+
raise McpError(ErrorData(INVALID_PARAMS, f"Unknown prompt: {name}"))
|
|
801
|
+
|
|
802
|
+
except Exception as e:
|
|
803
|
+
return GetPromptResult(
|
|
804
|
+
description=f"Operation failed: {str(e)}",
|
|
805
|
+
messages=[
|
|
806
|
+
PromptMessage(
|
|
807
|
+
role="user",
|
|
808
|
+
content=TextContent(type="text", text=str(e)),
|
|
809
|
+
)
|
|
810
|
+
],
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
options = server.create_initialization_options()
|
|
814
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
815
|
+
await server.run(read_stream, write_stream, options, raise_exceptions=True)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
if __name__ == "__main__":
|
|
819
|
+
import asyncio
|
|
820
|
+
import os
|
|
821
|
+
from dotenv import load_dotenv
|
|
822
|
+
|
|
823
|
+
load_dotenv()
|
|
824
|
+
|
|
825
|
+
api_key = os.getenv("LOGSEQ_API_TOKEN")
|
|
826
|
+
if not api_key:
|
|
827
|
+
raise ValueError("LOGSEQ_API_TOKEN environment variable is required")
|
|
828
|
+
|
|
829
|
+
url = os.getenv("LOGSEQ_API_URL")
|
|
830
|
+
if not url:
|
|
831
|
+
url = "http://localhost:12315"
|
|
832
|
+
|
|
833
|
+
asyncio.run(serve(api_key, url))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-server-logseq
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An MCP server for LogSeq API
|
|
5
|
+
Project-URL: homepage, https://github.com/dailydaniel/logseq-mcp
|
|
6
|
+
Project-URL: repository, https://github.com/dailydaniel/logseq-mcp
|
|
7
|
+
Project-URL: issues, https://github.com/dailydaniel/logseq-mcp/issues
|
|
8
|
+
Author-email: Daniel Zholkovsky <daniel@zholkovsky.com>
|
|
9
|
+
Maintainer-email: Daniel Zholkovsky <daniel@zholkovsky.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: automation,http,llm,mcp
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.10.2
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
22
|
+
Requires-Dist: requests>=2.32.3
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Logseq MCP Server
|
|
26
|
+
A Model Context Protocol server that provides direct integration with Logseq's knowledge base. This server enables LLMs to interact with Logseq graphs, create pages, manage blocks, and organize information programmatically.
|
|
27
|
+
|
|
28
|
+
## Available Tools
|
|
29
|
+
|
|
30
|
+
### logseq_insert_block - Creates new blocks in Logseq
|
|
31
|
+
**Parameters:**
|
|
32
|
+
- `parent_block` (string, optional): UUID or content of parent block/page
|
|
33
|
+
- `content` (string, required): Block content in Markdown/Org format
|
|
34
|
+
- `is_page_block` (boolean, optional): Create as page-level block (default: false)
|
|
35
|
+
- `before` (boolean, optional): Insert before parent block (default: false)
|
|
36
|
+
- `custom_uuid` (string, optional): Custom UUIDv4 for the block
|
|
37
|
+
|
|
38
|
+
### logseq_create_page - Creates new pages with properties
|
|
39
|
+
**Parameters:**
|
|
40
|
+
- `page_name` (string, required): Name of the page to create
|
|
41
|
+
- `properties` (object, optional): Page properties as key-value pairs
|
|
42
|
+
- `journal` (boolean, optional): Create as journal page (default: false)
|
|
43
|
+
- `format` (string, optional): Page format - "markdown" or "org" (default: "markdown")
|
|
44
|
+
- `create_first_block` (boolean, optional): Create initial empty block (default: true)
|
|
45
|
+
|
|
46
|
+
## Prompts
|
|
47
|
+
|
|
48
|
+
### logseq_insert_block
|
|
49
|
+
Create a new block in Logseq
|
|
50
|
+
**Arguments:**
|
|
51
|
+
- `parent_block`: Parent block reference (page name or UUID)
|
|
52
|
+
- `content`: Block content
|
|
53
|
+
- `is_page_block`: Set true for page-level blocks
|
|
54
|
+
|
|
55
|
+
### logseq_create_page
|
|
56
|
+
Create a new Logseq page
|
|
57
|
+
**Arguments:**
|
|
58
|
+
- `page_name`: Name of the page
|
|
59
|
+
- `properties`: Page properties as JSON
|
|
60
|
+
- `journal`: Set true for journal pages
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### Using pip
|
|
65
|
+
todo: add to pypi
|
|
66
|
+
### From source
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/dailydaniel/logseq-mcp.git
|
|
69
|
+
cd logseq-mcp
|
|
70
|
+
cp .env.example .env
|
|
71
|
+
uv sync
|
|
72
|
+
```
|
|
73
|
+
Run the server:
|
|
74
|
+
```bash
|
|
75
|
+
python -m mcp_server_logseq
|
|
76
|
+
```
|
|
77
|
+
## Configuration
|
|
78
|
+
### API Key
|
|
79
|
+
1. Generate API token in Logseq: API → Authorization tokens
|
|
80
|
+
2. Set environment variable:
|
|
81
|
+
```bash
|
|
82
|
+
export LOGSEQ_API_TOKEN=your_token_here
|
|
83
|
+
```
|
|
84
|
+
Or pass via command line:
|
|
85
|
+
```bash
|
|
86
|
+
python -m mcp_server_logseq --api-key=your_token_here
|
|
87
|
+
```
|
|
88
|
+
### Graph Configuration
|
|
89
|
+
Default URL: http://localhost:12315
|
|
90
|
+
To customize:
|
|
91
|
+
```bash
|
|
92
|
+
python -m mcp_server_logseq --url=http://your-logseq-instance:port
|
|
93
|
+
```
|
|
94
|
+
## Examples
|
|
95
|
+
## Create meeting notes page
|
|
96
|
+
```plaintext
|
|
97
|
+
Create new page "Team Meeting 2024-03-15" with properties:
|
|
98
|
+
- Tags: #meeting #engineering
|
|
99
|
+
- Participants: Alice, Bob, Charlie
|
|
100
|
+
- Status: pending
|
|
101
|
+
```
|
|
102
|
+
### Add task block to existing page
|
|
103
|
+
```plaintext
|
|
104
|
+
Add task to [[Project Roadmap]]:
|
|
105
|
+
- [ ] Finalize API documentation
|
|
106
|
+
- Due: 2024-03-20
|
|
107
|
+
- Priority: high
|
|
108
|
+
```
|
|
109
|
+
### Create journal entry with first block
|
|
110
|
+
```plaintext
|
|
111
|
+
Create journal entry for today with initial content:
|
|
112
|
+
- Morning standup completed
|
|
113
|
+
- Started work on new authentication system
|
|
114
|
+
```
|
|
115
|
+
## Debugging
|
|
116
|
+
```bash
|
|
117
|
+
npx @modelcontextprotocol/inspector uv --directory . run mcp-server-logseq
|
|
118
|
+
```
|
|
119
|
+
## Contributing
|
|
120
|
+
We welcome contributions to enhance Logseq integration:
|
|
121
|
+
- Add new API endpoints (page linking, query support)
|
|
122
|
+
- Improve block manipulation capabilities
|
|
123
|
+
- Add template support
|
|
124
|
+
- Enhance error handling
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mcp_server_logseq/__init__.py,sha256=OXIjFRngmRTygJWhtcxe5tlW2RHOElZDyQqVFQSvbdg,1158
|
|
2
|
+
mcp_server_logseq/__main__.py,sha256=ycwmbLPyzbJv61y-4znX8sDhtUB_h3luB3Ovwy3MUig,58
|
|
3
|
+
mcp_server_logseq/server.py,sha256=z_uIlOho432f0UKmvQT_v7cIyL1YK2R3YLEFghYM5Fo,30334
|
|
4
|
+
mcp_server_logseq-0.0.1.dist-info/METADATA,sha256=s4xXzYEKz7WUklFAzNU4xaiAQlV7Dy7Z5OqRNKJiVB0,3970
|
|
5
|
+
mcp_server_logseq-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
mcp_server_logseq-0.0.1.dist-info/entry_points.txt,sha256=gZmJrfV_SyjnvH7XSrUfEple2L36nzF2GhA6BSUUP_Y,70
|
|
7
|
+
mcp_server_logseq-0.0.1.dist-info/licenses/LICENSE,sha256=sL2jKeHFd2DXT4leTz-fv5ZN9LsFhYIqOyIH7b8hhp0,1063
|
|
8
|
+
mcp_server_logseq-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniel
|
|
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.
|