Skip to content

Commit 1560b2a

Browse files
committed
Enhance Notion search behavior, defaults, and tool docs
- Fix enhanced search flow and early return; try multiple query variations and deduplicate results - Improve search variation generation and limit extra queries - Increase DEFAULT_SEARCH_BLOCK_LIMIT from 20 to 100 - Surface "PAGE ID" consistently in search headings and page summaries - Default notion_retrieve_page to include_children=True and clarify PAGE ID vs block ID in docs - Add/expand MCP tool descriptions for notion_search, notion_retrieve_page, notion_create_page, notion_append_block_children, and notion_update_block - Misc: whitespace/formatting cleanup and minor docstring refinements
1 parent 1cf45b0 commit 1560b2a

1 file changed

Lines changed: 96 additions & 48 deletions

File tree

src/backend/mcp_servers/notion_server.py

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@
66
77
Tools provided
88
--------------
9-
* ``notion_search`` – search for reminders, notes, and stored information (e.g.,
9+
* ``notion_search`` – search for reminders, notes, and stored information (e.g.,
1010
"names to remember", "project ideas"). Wraps the `/v1/search` endpoint.
1111
* ``notion_retrieve_page`` – retrieve detailed content from a reminder or note page,
12-
fetching metadata and block content via `/v1/pages/{page_id}` and
12+
fetching metadata and block content via `/v1/pages/{page_id}` and
1313
`/v1/blocks/{page_id}/children`.
1414
* ``notion_create_page`` – create new reminder notes or memory storage pages (e.g.,
1515
"Names to Remember", "Books to Read") using `/v1/pages`.
1616
* ``notion_append_block_children`` – add new content to existing reminders or notes
17-
(e.g., add a new name to your "names to remember" page) via
17+
(e.g., add a new name to your "names to remember" page) via
1818
`/v1/blocks/{block_id}/children`.
1919
* ``notion_update_block`` – update or modify content in existing reminder blocks
2020
via `/v1/blocks/{block_id}`.
2121
2222
Common use cases
2323
----------------
24-
* Remember names: Create a "Names to Remember" page, search it when needed, and
24+
* Remember names: Create a "Names to Remember" page, search it when needed, and
2525
add new names as you meet people.
26-
* Store information: Create topic-specific notes (e.g., "Project Ideas",
26+
* Store information: Create topic-specific notes (e.g., "Project Ideas",
2727
"Books to Read") that you can search and update later.
28-
* Manage reminders: Build lists and notes that help you remember important
28+
* Manage reminders: Build lists and notes that help you remember important
2929
information, tasks, or ideas.
3030
3131
Required environment variables
@@ -91,7 +91,7 @@ def run(self) -> None: ...
9191

9292
NOTION_BASE_URL = "https://api.notion.com/v1"
9393
DEFAULT_NOTION_VERSION = "2022-06-28"
94-
DEFAULT_SEARCH_BLOCK_LIMIT = 20
94+
DEFAULT_SEARCH_BLOCK_LIMIT = 100
9595

9696

9797
@dataclass(slots=True)
@@ -399,7 +399,7 @@ def _format_result_heading(entry: Dict[str, Any]) -> str:
399399
object_type = entry.get("object", "unknown").title()
400400
identifier = entry.get("id", "(unknown id)")
401401
title = _extract_title(entry) or "(untitled)"
402-
return f"{object_type}{title} • ID: {identifier}"
402+
return f"{object_type}{title}PAGE ID: {identifier}"
403403

404404

405405
def _format_search_results(
@@ -426,12 +426,13 @@ def _format_search_results(
426426

427427
def _format_page_summary(page: Dict[str, Any]) -> str:
428428
title = _extract_title(page) or "(untitled)"
429+
page_id = page.get("id", "(unknown id)")
429430
url = page.get("url")
430431
last_edited = page.get("last_edited_time")
431432
created_time = page.get("created_time")
432433
properties = page.get("properties", {})
433434

434-
lines = [f"Title: {title}"]
435+
lines = [f"Title: {title}", f"PAGE ID: {page_id}"]
435436
if url:
436437
lines.append(f"URL: {url}")
437438
if created_time:
@@ -531,38 +532,38 @@ async def _collect_search_details(
531532

532533
def _generate_search_variations(query: Optional[str]) -> List[str]:
533534
"""Generate search query variations to improve match likelihood.
534-
535+
535536
This function creates multiple search strategies:
536537
1. The original query as-is
537538
2. Individual significant words (4+ characters) from the query
538539
3. Common phrase patterns
539-
540+
540541
This helps overcome Notion API's exact-match limitations.
541542
"""
542543
if not query:
543544
return []
544-
545+
545546
variations = [query.strip()]
546-
547+
547548
# Split query into words and add significant terms individually
548549
words = query.strip().lower().split()
549-
550+
550551
# Filter for words that are 4+ characters (skip "to", "at", "the", "and", etc.)
551552
significant_words = [w for w in words if len(w) >= 4]
552-
553+
553554
# Add individual significant words as search variations
554555
for word in significant_words:
555556
if word not in variations:
556557
variations.append(word)
557-
558+
558559
# Add common multi-word combinations if query has multiple words
559560
if len(significant_words) >= 2:
560561
# Try pairs of adjacent words
561562
for i in range(len(significant_words) - 1):
562563
pair = f"{significant_words[i]} {significant_words[i + 1]}"
563564
if pair not in variations:
564565
variations.append(pair)
565-
566+
566567
return variations
567568

568569

@@ -576,10 +577,10 @@ async def _perform_enhanced_search(
576577
max_variations: int = 3,
577578
) -> tuple[List[Dict[str, Any]], Optional[str]]:
578579
"""Perform enhanced search with multiple query variations.
579-
580+
580581
Tries the original query first, and if results are insufficient,
581582
attempts searches with query variations to find more relevant matches.
582-
583+
583584
Returns deduplicated results and the next cursor from the best search.
584585
"""
585586
# Try the original query first
@@ -593,14 +594,14 @@ async def _perform_enhanced_search(
593594
response = await _request("POST", "/search", json=payload)
594595
results = response.get("results") or []
595596
next_cursor = response.get("next_cursor")
596-
597+
597598
# If we got good results or there's no query, return immediately
598599
if len(results) >= 3 or not query or start_cursor:
599600
return results, next_cursor
600-
601+
601602
# Try search variations to find more matches
602603
variations = _generate_search_variations(query)
603-
604+
604605
# Skip the first variation (original query) since we already tried it
605606
# Limit the number of additional API calls
606607
for variation in variations[1:max_variations]:
@@ -613,26 +614,45 @@ async def _perform_enhanced_search(
613614
try:
614615
variation_response = await _request("POST", "/search", json=variation_payload)
615616
variation_results = variation_response.get("results") or []
616-
617+
617618
# Deduplicate results by ID
618619
existing_ids = {r.get("id") for r in results}
619620
for result in variation_results:
620621
result_id = result.get("id")
621622
if result_id and result_id not in existing_ids:
622623
results.append(result)
623624
existing_ids.add(result_id)
624-
625+
625626
# If we now have enough results, stop searching
626627
if len(results) >= 5:
627628
break
628629
except NotionAPIError:
629630
# If a variation search fails, continue with others
630631
continue
631-
632+
632633
return results, next_cursor
633634

634635

635-
@mcp.tool("notion_search")
636+
@mcp.tool(
637+
"notion_search",
638+
description=(
639+
"Search Notion for reminders, notes, and information you want to remember. "
640+
"Returns matching pages with their PAGE IDs (look for 'ID: ...' in results). "
641+
"Use this to find pages like 'Names to Remember', 'Project Ideas', or any stored information. "
642+
"When searching for specific details (like someone's name), this will return relevant pages WITH their content. "
643+
"If you see 'Additional blocks available' in the results, the content was truncated - "
644+
"immediately call notion_retrieve_page using the PAGE ID (not block IDs) with include_children=true to get ALL content. "
645+
"\n\n"
646+
"CRITICAL SEARCH STRATEGY for finding specific information: "
647+
"When user asks about a specific person/thing (e.g., 'who is the old lady at the park'), "
648+
"DO NOT search for that exact phrase. Instead: "
649+
"1. Search for the relevant page by title (e.g., 'Names' or 'Names to Remember'). "
650+
"2. Read through ALL the returned content to find matching entries. "
651+
"3. If no results or truncated, use notion_retrieve_page to get COMPLETE content. "
652+
"Notion search is literal - 'old lady at the park' won't match 'old lady park' in the content. "
653+
"Always retrieve the full page and search through it yourself for specific details."
654+
)
655+
)
636656
async def notion_search(
637657
query: Optional[str] = None,
638658
*,
@@ -650,21 +670,17 @@ async def notion_search(
650670
- Finding notes about specific topics or subjects
651671
- Retrieving stored reminders and memory aids
652672
- Looking up information you've saved for later recall
653-
654-
This search now uses an enhanced multi-strategy approach that tries query variations
655-
to overcome Notion API's exact-match limitations, making it better at finding
656-
relevant pages even with partial or fuzzy queries.
657-
673+
658674
Examples:
659-
- Search "names to remember" to find a note containing names
675+
- Search "names to remember" or "Names" to find a note containing names
660676
- Search "project ideas" to retrieve saved project notes
661677
- Search "books to read" to find your reading list
662-
678+
663679
Authentication requires ``NOTION_TOKEN`` (preferred) or ``NOTION_API_KEY`` to
664680
be present in the environment. Optional ``NOTION_VERSION`` mirrors the
665681
upstream configuration and defaults to ``2022-06-28``. Set
666682
``include_content=False`` to return metadata only. ``content_block_limit``
667-
controls how many child blocks are retrieved per page (defaults to 20).
683+
controls how many child blocks are retrieved per page (defaults to 100).
668684
"""
669685

670686
# Use enhanced search that tries multiple query variations
@@ -675,7 +691,7 @@ async def notion_search(
675691
start_cursor=start_cursor,
676692
page_size=page_size,
677693
)
678-
694+
679695
block_limit = max(1, content_block_limit or DEFAULT_SEARCH_BLOCK_LIMIT)
680696
details = (
681697
await _collect_search_details(results, block_limit=block_limit)
@@ -685,25 +701,37 @@ async def notion_search(
685701
return _format_search_results(results, details, next_cursor)
686702

687703

688-
@mcp.tool("notion_retrieve_page")
704+
@mcp.tool(
705+
"notion_retrieve_page",
706+
description=(
707+
"Retrieve the COMPLETE content of a specific Notion page/note by its PAGE ID. "
708+
"IMPORTANT: Use the PAGE ID from search results (e.g., 'ID: 29896b0b-3790-8118-...'), NOT block IDs. "
709+
"Use this when notion_search returns truncated content and you need ALL blocks from the page. "
710+
"Perfect for reading entire 'Names to Remember' lists or any page where you need to search through ALL entries. "
711+
"Always use include_children=true when you need to find specific information within a page."
712+
)
713+
)
689714
async def notion_retrieve_page(
690715
page_id: str,
691716
*,
692717
filter_properties: Optional[List[str]] = None,
693-
include_children: bool = False,
718+
include_children: bool = True,
694719
start_cursor: Optional[str] = None,
695720
page_size: Optional[int] = None,
696721
) -> str:
697722
"""Retrieve detailed content from a reminder or note page in Notion.
698723
724+
IMPORTANT: page_id must be the PAGE ID from search results (e.g., '29896b0b-3790-8118-b115-e843978e56ba'),
725+
NOT a block ID (which appears in parentheses after block types like 'Paragraph (block-id)').
726+
699727
Use this tool to read the full content of a specific reminder, note, or stored information.
700728
Perfect for accessing complete details after finding a page via search.
701-
729+
702730
Common use cases:
703731
- Read all names from a "names to remember" note
704732
- Review detailed information from a reminder page
705733
- Check the full content of a note you've found
706-
734+
707735
Set ``include_children=True`` to fetch the complete page content with all blocks.
708736
Use ``start_cursor`` and ``page_size`` to paginate through long documents.
709737
@@ -735,7 +763,14 @@ async def notion_retrieve_page(
735763
return f"{summary}\n\n{blocks_output}"
736764

737765

738-
@mcp.tool("notion_create_page")
766+
@mcp.tool(
767+
"notion_create_page",
768+
description=(
769+
"Create a new reminder note or memory storage page in Notion. "
770+
"Use this to create pages like 'Names to Remember', 'Books to Read', 'Project Ideas', etc. "
771+
"You can set a title and optionally add initial content blocks."
772+
)
773+
)
739774
async def notion_create_page(
740775
data: NotionCreatePageInput,
741776
) -> str:
@@ -747,14 +782,14 @@ async def notion_create_page(
747782
- Saving reminders about tasks or things to do
748783
- Creating notes about topics you want to remember
749784
- Storing information for future reference
750-
785+
751786
Examples:
752787
- Create a page titled "Names to Remember" with initial names
753788
- Create a "Project Ideas" page to store your ideas
754789
- Create reminder notes with titles like "Things to Buy" or "Books to Read"
755-
756-
If ``parent_id`` is omitted the server will fall back to ``NOTION_DATABASE_ID``
757-
or ``NOTION_PAGE_ID``. Provide ``title`` for simple notes or supply ``properties``
790+
791+
If ``parent_id`` is omitted the server will fall back to ``NOTION_DATABASE_ID``
792+
or ``NOTION_PAGE_ID``. Provide ``title`` for simple notes or supply ``properties``
758793
that match your database schema when creating structured entries.
759794
760795
Authentication requires ``NOTION_TOKEN`` (preferred) or ``NOTION_API_KEY``.
@@ -785,15 +820,22 @@ def _build_children_payload(data: NotionAppendChildrenInput) -> Dict[str, Any]:
785820
return {"children": children}
786821

787822

788-
@mcp.tool("notion_append_block_children")
823+
@mcp.tool(
824+
"notion_append_block_children",
825+
description=(
826+
"Add new content to an existing Notion page/note. "
827+
"Use this to append new entries to lists like adding a new name to 'Names to Remember', "
828+
"a new book to 'Books to Read', or any new reminder to an existing page."
829+
)
830+
)
789831
async def notion_append_block_children(data: NotionAppendChildrenInput) -> str:
790832
"""Add new content to an existing reminder or note page in Notion.
791833
792834
Use this tool to add more information to existing notes or reminders. Perfect for:
793835
- Adding a new name to your "names to remember" note
794836
- Appending new items to an existing reminder list
795837
- Adding additional information to a note you've already created
796-
838+
797839
Examples:
798840
- Add "John Smith - met at conference" to your names note
799841
- Append new book titles to your reading list
@@ -835,7 +877,13 @@ def _build_block_update_payload(data: NotionUpdateBlockInput) -> Dict[str, Any]:
835877
return payload
836878

837879

838-
@mcp.tool("notion_update_block")
880+
@mcp.tool(
881+
"notion_update_block",
882+
description=(
883+
"Update or modify existing content in a Notion reminder or note. "
884+
"Use this to correct information, update details, or archive old reminders."
885+
)
886+
)
839887
async def notion_update_block(data: NotionUpdateBlockInput) -> str:
840888
"""Update or modify content in an existing reminder or note block.
841889
@@ -844,7 +892,7 @@ async def notion_update_block(data: NotionUpdateBlockInput) -> str:
844892
- Updating a name with additional context or corrections
845893
- Modifying reminder text to reflect changes
846894
- Correcting or enhancing stored information
847-
895+
848896
Examples:
849897
- Update "John" to "John Smith - CEO at Tech Corp"
850898
- Change a reminder note with updated details

0 commit comments

Comments
 (0)