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.
@@ -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,5 @@
1
+ # __main__.py
2
+
3
+ from mcp_server_logseq import main
4
+
5
+ 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-server-logseq = mcp_server_logseq.__main__:main
@@ -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.