13
13
from contextlib import asynccontextmanager
14
14
from copy import deepcopy
15
15
from dataclasses import dataclass
16
+ from functools import partial
16
17
from http import HTTPStatus
17
18
from typing import cast , AsyncIterator
18
19
from urllib .parse import urlencode , quote , unquote_plus
19
20
20
21
from fastapi import FastAPI , Request
21
22
from mcp .server import FastMCP
22
23
from mcp .server .fastmcp import Context
23
- from mcp .server .session import ServerSessionT
24
24
from pydantic import Field
25
25
from starlette .datastructures import QueryParams
26
26
from uvicorn import Config , Server
27
27
from uvicorn .config import LOGGING_CONFIG
28
28
29
29
BASE_URL = "bear://x-callback-url"
30
30
31
+ LOGGER = logging .getLogger (__name__ )
32
+
31
33
32
34
@dataclass
33
35
class ErrorResponse (Exception ):
@@ -79,53 +81,52 @@ class AppContext:
79
81
grab_url_results : Queue [Future [QueryParams ]]
80
82
81
83
82
- def create_server (token : str , callback_host : str , callback_port : int ) -> FastMCP :
83
- logger = logging .getLogger (__name__ )
84
-
85
- @asynccontextmanager
86
- async def app_lifespan (_server : FastMCP ) -> AsyncIterator [AppContext ]:
87
- callback = FastAPI ()
88
-
89
- log_config = deepcopy (LOGGING_CONFIG )
90
- log_config ["handlers" ]["access" ]["stream" ] = "ext://sys.stderr"
91
- server = Server (
92
- Config (
93
- app = callback ,
94
- host = callback_host ,
95
- port = callback_port ,
96
- log_level = "warning" ,
97
- log_config = log_config ,
98
- )
84
+ @asynccontextmanager
85
+ async def app_lifespan (_server : FastMCP , callback_host : str , callback_port : int ) -> AsyncIterator [AppContext ]:
86
+ callback = FastAPI ()
87
+
88
+ log_config = deepcopy (LOGGING_CONFIG )
89
+ log_config ["handlers" ]["access" ]["stream" ] = "ext://sys.stderr"
90
+ server = Server (
91
+ Config (
92
+ app = callback ,
93
+ host = callback_host ,
94
+ port = callback_port ,
95
+ log_level = "warning" ,
96
+ log_config = log_config ,
99
97
)
98
+ )
99
+
100
+ LOGGER .info (f"Starting callback server on { callback_host } :{ callback_port } " )
101
+ server_task = asyncio .create_task (server .serve ())
102
+ try :
103
+ yield AppContext (
104
+ open_note_results = register_callback (callback , "open-note" ),
105
+ create_results = register_callback (callback , "create" ),
106
+ tags_results = register_callback (callback , "tags" ),
107
+ open_tag_results = register_callback (callback , "open-tag" ),
108
+ todo_results = register_callback (callback , "todo" ),
109
+ today_results = register_callback (callback , "today" ),
110
+ search_results = register_callback (callback , "search" ),
111
+ grab_url_results = register_callback (callback , "grab-url" ),
112
+ )
113
+ finally :
114
+ LOGGER .info ("Stopping callback server" )
115
+ server .should_exit = True
116
+ await server_task
100
117
101
- logger .info (f"Starting callback server on { callback_host } :{ callback_port } " )
102
- server_task = asyncio .create_task (server .serve ())
103
- try :
104
- yield AppContext (
105
- open_note_results = register_callback (callback , "open-note" ),
106
- create_results = register_callback (callback , "create" ),
107
- tags_results = register_callback (callback , "tags" ),
108
- open_tag_results = register_callback (callback , "open-tag" ),
109
- todo_results = register_callback (callback , "todo" ),
110
- today_results = register_callback (callback , "today" ),
111
- search_results = register_callback (callback , "search" ),
112
- grab_url_results = register_callback (callback , "grab-url" ),
113
- )
114
- finally :
115
- logger .info ("Stopping callback server" )
116
- server .should_exit = True
117
- await server_task
118
118
119
- mcp = FastMCP ("Bear" , lifespan = app_lifespan )
119
+ def create_server (token : str , callback_host : str , callback_port : int ) -> FastMCP :
120
+ mcp = FastMCP ("Bear" , lifespan = partial (app_lifespan , callback_host = callback_host , callback_port = callback_port ))
120
121
121
122
@mcp .tool ()
122
123
async def open_note (
123
- ctx : Context [ ServerSessionT , AppContext ] ,
124
+ ctx : Context ,
124
125
id : str | None = Field (description = "note unique identifier" , default = None ),
125
126
title : str | None = Field (description = "note title" , default = None ),
126
127
) -> str :
127
128
"""Open a note identified by its title or id and return its content."""
128
- app_ctx = ctx .request_context .lifespan_context
129
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
129
130
future = Future [QueryParams ]()
130
131
await app_ctx .open_note_results .put (future )
131
132
@@ -152,14 +153,14 @@ async def open_note(
152
153
153
154
@mcp .tool ()
154
155
async def create (
155
- ctx : Context [ ServerSessionT , AppContext ] ,
156
+ ctx : Context ,
156
157
title : str | None = Field (description = "note title" , default = None ),
157
158
text : str | None = Field (description = "note body" , default = None ),
158
159
tags : list [str ] | None = Field (description = "list of tags" , default = None ),
159
160
timestamp : bool = Field (description = "prepend the current date and time to the text" , default = False ),
160
161
) -> str :
161
162
"""Create a new note and return its unique identifier. Empty notes are not allowed."""
162
- app_ctx = ctx .request_context .lifespan_context
163
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
163
164
future = Future [QueryParams ]()
164
165
await app_ctx .create_results .put (future )
165
166
@@ -187,10 +188,10 @@ async def create(
187
188
188
189
@mcp .tool ()
189
190
async def tags (
190
- ctx : Context [ ServerSessionT , AppContext ] ,
191
+ ctx : Context ,
191
192
) -> list [str ]:
192
193
"""Return all the tags currently displayed in Bear’s sidebar."""
193
- app_ctx = ctx .request_context .lifespan_context
194
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
194
195
future = Future [QueryParams ]()
195
196
await app_ctx .tags_results .put (future )
196
197
@@ -208,11 +209,11 @@ async def tags(
208
209
209
210
@mcp .tool ()
210
211
async def open_tag (
211
- ctx : Context [ ServerSessionT , AppContext ] ,
212
+ ctx : Context ,
212
213
name : str = Field (description = "tag name or a list of tags divided by comma" ),
213
214
) -> list [str ]:
214
215
"""Show all the notes which have a selected tag in bear."""
215
- app_ctx = ctx .request_context .lifespan_context
216
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
216
217
future = Future [QueryParams ]()
217
218
await app_ctx .open_tag_results .put (future )
218
219
@@ -231,11 +232,11 @@ async def open_tag(
231
232
232
233
@mcp .tool ()
233
234
async def todo (
234
- ctx : Context [ ServerSessionT , AppContext ] ,
235
+ ctx : Context ,
235
236
search : str | None = Field (description = "string to search" , default = None ),
236
237
) -> list [str ]:
237
238
"""Select the Todo sidebar item."""
238
- app_ctx = ctx .request_context .lifespan_context
239
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
239
240
future = Future [QueryParams ]()
240
241
await app_ctx .todo_results .put (future )
241
242
@@ -256,11 +257,11 @@ async def todo(
256
257
257
258
@mcp .tool ()
258
259
async def today (
259
- ctx : Context [ ServerSessionT , AppContext ] ,
260
+ ctx : Context ,
260
261
search : str | None = Field (description = "string to search" , default = None ),
261
262
) -> list [str ]:
262
263
"""Select the Today sidebar item."""
263
- app_ctx = ctx .request_context .lifespan_context
264
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
264
265
future = Future [QueryParams ]()
265
266
await app_ctx .today_results .put (future )
266
267
@@ -281,12 +282,12 @@ async def today(
281
282
282
283
@mcp .tool ()
283
284
async def search (
284
- ctx : Context [ ServerSessionT , AppContext ] ,
285
+ ctx : Context ,
285
286
term : str | None = Field (description = "string to search" , default = None ),
286
287
tag : str | None = Field (description = "tag to search into" , default = None ),
287
288
) -> list [str ]:
288
289
"""Show search results in Bear for all notes or for a specific tag."""
289
- app_ctx = ctx .request_context .lifespan_context
290
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
290
291
future = Future [QueryParams ]()
291
292
await app_ctx .search_results .put (future )
292
293
@@ -309,15 +310,15 @@ async def search(
309
310
310
311
@mcp .tool ()
311
312
async def grab_url (
312
- ctx : Context [ ServerSessionT , AppContext ] ,
313
+ ctx : Context ,
313
314
url : str = Field (description = "url to grab" ),
314
315
tags : list [str ] | None = Field (
315
316
description = "list of tags. If tags are specified in the Bear’s web content preferences, this parameter is ignored." ,
316
317
default = None ,
317
318
),
318
319
) -> str :
319
320
"""Create a new note with the content of a web page and return its unique identifier."""
320
- app_ctx = ctx .request_context .lifespan_context
321
+ app_ctx : AppContext = ctx .request_context .lifespan_context # type: ignore
321
322
future = Future [QueryParams ]()
322
323
await app_ctx .grab_url_results .put (future )
323
324
0 commit comments