Skip to content

Commit 0a53f04

Browse files
Add ctx.api and ctx.call_api to MCP context (#1241)
1 parent b0e12cc commit 0a53f04

File tree

5 files changed

+63
-69
lines changed

5 files changed

+63
-69
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Releases prior to 7.0 has been removed from this file to declutter search result
1212

1313
- cli: Added `init --no-base` option to skip creating the base template.
1414
- env: Added `DIPDUP_NO_BASE` environment variable to skip creating the base template.
15+
- mcp: Added `ctx.api` datasource and `ctx.call_api` helper to server context.
1516

1617
### Fixed
1718

docs/5.advanced/7.mcp.md

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ This tool will:
151151

152152
### Using application context
153153

154-
You can use the application context the same way as in handlers and hooks. Use the `mcp.get_ctx()` function to get the context object.
154+
You can use the application context the same way as in handlers and hooks with a few differences. There is a single context object per server process and it's not passed as a first argument to callbacks.
155+
156+
Use the `mcp.get_ctx()` function to get the context object.
155157

156158
```python [mcp/tools.py]
157159
from dipdup import mcp
@@ -180,34 +182,26 @@ For a low-level access you can use `dipdup.mcp.server` singleton to interact wit
180182

181183
### Interacting with running indexer
182184

183-
DipDup provides [management API](../7.references/4.api.md) to interact with the running indexer. For example you can use it to add indexes in runtime. First, add running indexer as a HTTP datasource:
185+
DipDup provides [management API](../7.references/4.api.md) to interact with the running indexer. For example you can use it to add indexes in runtime.
184186

185-
```yaml [dipdup.yaml]
186-
datasources:
187-
indexer:
188-
kind: http
189-
# NOTE: Default for Compose stack
190-
url: http://api:46339
191-
```
192-
193-
Then, call this datasource from your MCP tool:
187+
You can use `ctx.call_api` to get nice plaintext output or `ctx.api` to access the datasource directly.
194188

195189
```python
196190
from dipdup import mcp
197191
198192
@mcp.tool(...)
199193
async def tool():
200194
ctx = mcp.get_ctx()
201-
datasource = ctx.get_http_datasource('indexer')
202-
response = await datasource.post(
203-
'/add_index',
204-
params={
205-
'name': 'my_index',
206-
'template': 'my_template',
207-
'values': {'param': 'value'},
208-
'first_level': 0,
209-
'last_level': 1000,
210-
}
195+
response = await ctx.call_api(
196+
method='post',
197+
path='/add_index',
198+
params={
199+
'name': 'my_index',
200+
'template': 'my_template',
201+
'values': {'param': 'value'},
202+
'first_level': 0,
203+
'last_level': 1000,
204+
}
211205
)
212206
```
213207

src/dipdup/cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,11 @@ async def mcp_run(ctx: click.Context) -> None:
567567

568568
from dipdup import mcp
569569
from dipdup.config import DipDupConfig
570+
from dipdup.config import HttpConfig
570571
from dipdup.config import McpConfig
572+
from dipdup.config.http import HttpDatasourceConfig
571573
from dipdup.context import McpContext
574+
from dipdup.datasources.http import HttpDatasource
572575
from dipdup.dipdup import DipDup
573576

574577
config: DipDupConfig = ctx.obj.config
@@ -578,10 +581,20 @@ async def mcp_run(ctx: click.Context) -> None:
578581
config.mcp = McpConfig()
579582
mcp_config = config.mcp
580583

584+
api_datasource_config = HttpDatasourceConfig(
585+
url=mcp_config.default_api_url,
586+
http=HttpConfig(
587+
retry_count=0,
588+
),
589+
)
590+
api_datasource_config._name = 'api'
591+
api_datasource = HttpDatasource(api_datasource_config)
592+
581593
mcp_ctx = McpContext._wrap(
582594
ctx=dipdup._ctx,
583595
logger=mcp._logger,
584596
server=mcp.server,
597+
api=api_datasource,
585598
)
586599
mcp.set_ctx(mcp_ctx)
587600

@@ -622,9 +635,13 @@ async def handle_sse(request: Any) -> None:
622635
logging.getLogger('mcp').setLevel(logging.INFO)
623636

624637
async with AsyncExitStack() as stack:
638+
# NOTE: Create, but doesn't initialize (no WS loop)
625639
await dipdup._create_datasources()
626640
await dipdup._set_up_database(stack)
627641

642+
# NOTE: Not available in `ctx.datasources`, but directly as `ctx.api`
643+
await stack.enter_async_context(api_datasource)
644+
628645
# NOTE: Run MCP in a separate thread to avoid blocking the DB connection
629646
portal = stack.enter_context(from_thread.start_blocking_portal())
630647
portal.call(server.serve)

src/dipdup/context.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ class McpContext(DipDupContext):
925925
:param transactions: Transaction manager (low-level interface)
926926
:param logger: Context-aware logger instance
927927
:param server: Running MCP server instance
928+
:param api: DipDup API datasource
928929
"""
929930

930931
def __init__(
@@ -935,6 +936,7 @@ def __init__(
935936
transactions: TransactionManager,
936937
logger: Logger,
937938
server: McpServer[Any],
939+
api: HttpDatasource,
938940
) -> None:
939941
super().__init__(
940942
config=config,
@@ -944,13 +946,15 @@ def __init__(
944946
)
945947
self.logger = logger
946948
self.server = server
949+
self.api = api
947950

948951
@classmethod
949952
def _wrap(
950953
cls,
951954
ctx: DipDupContext,
952955
logger: Logger,
953956
server: Any,
957+
api: Any,
954958
) -> Self:
955959
new_ctx = cls(
956960
config=ctx.config,
@@ -959,6 +963,25 @@ def _wrap(
959963
transactions=ctx.transactions,
960964
logger=logger,
961965
server=server,
966+
api=api,
962967
)
963968
ctx._link(new_ctx)
964969
return new_ctx
970+
971+
async def call_api(
972+
self,
973+
method: str,
974+
path: str,
975+
params: dict[str, Any] | None = None,
976+
) -> str:
977+
_logger.info('Calling API: %s %s', method, path)
978+
979+
res = await self.api.request(
980+
method=method,
981+
url=path.lstrip('/'),
982+
json={k: v for k, v in (params or {}).items() if v is not None},
983+
raw=True,
984+
)
985+
if res.status != 200:
986+
return f'ERROR: {res.status} {res.reason}'
987+
return await res.text() # type: ignore[no-any-return]

src/dipdup/mcp.py

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
T = TypeVar('T', bound=Callable[..., Awaitable[Any]])
1515

1616
from dipdup import models
17-
from dipdup.context import McpContext
17+
from dipdup.context import McpContext as McpContext
1818
from dipdup.exceptions import FrameworkException
1919
from dipdup.utils import json_dumps
2020

@@ -73,16 +73,9 @@ async def _resource_indexes() -> list[dict[str, Any]]:
7373
return res
7474

7575

76-
def _get_indexer_url() -> str:
77-
from dipdup.config import McpConfig
78-
79-
ctx = get_ctx()
80-
return (ctx.config.mcp or McpConfig()).default_api_url
81-
82-
8376
async def _tool_api_config() -> str:
84-
return await api_call(
85-
url=_get_indexer_url(),
77+
ctx = get_ctx()
78+
return await ctx.call_api(
8679
method='get',
8780
path='/config',
8881
)
@@ -95,8 +88,8 @@ async def _tool_api_add_contract(
9588
typename: str | None = None,
9689
code_hash: str | int | None = None,
9790
) -> str:
98-
await api_call(
99-
url=_get_indexer_url(),
91+
ctx = get_ctx()
92+
await ctx.call_api(
10093
method='post',
10194
path='/add_contract',
10295
params={
@@ -117,8 +110,8 @@ async def _tool_api_add_index(
117110
first_level: int | None = None,
118111
last_level: int | None = None,
119112
) -> str:
120-
await api_call(
121-
url=_get_indexer_url(),
113+
ctx = get_ctx()
114+
await ctx.call_api(
122115
method='post',
123116
path='/add_index',
124117
params={
@@ -187,40 +180,6 @@ def set_ctx(ctx: McpContext) -> None:
187180
_ctx = ctx
188181

189182

190-
async def api_call(
191-
url: str,
192-
method: str,
193-
path: str,
194-
params: dict[str, Any] | None = None,
195-
) -> str:
196-
from dipdup.config import HttpConfig
197-
from dipdup.config.http import HttpDatasourceConfig
198-
from dipdup.datasources.http import HttpDatasource
199-
200-
_logger.info('Calling API: %s %s', method, url + path)
201-
202-
config = HttpDatasourceConfig(
203-
kind='http',
204-
url=url,
205-
http=HttpConfig(
206-
retry_count=0,
207-
),
208-
)
209-
config._name = 'dipdup_api'
210-
211-
datasource = HttpDatasource(config)
212-
async with datasource:
213-
res = await datasource.request(
214-
method=method,
215-
url=path.lstrip('/'),
216-
json={k: v for k, v in (params or {}).items() if v is not None},
217-
raw=True,
218-
)
219-
if res.status != 200:
220-
return f'ERROR: {res.status} {res.reason}'
221-
return await res.text() # type: ignore[no-any-return]
222-
223-
224183
# TODO: Add instructions
225184
server: mcp.server.Server[Any] = mcp.server.Server(name='DipDup')
226185
_user_tools: dict[str, types.Tool] = {}

0 commit comments

Comments
 (0)