Skip to content

Commit cfd6647

Browse files
authored
MCP ragnar server (#123)
* add `mcp_serve_store` * opt-out of r session tools Companion to posit-dev/mcptools#68 * update docs + small tweaks * Change the default tool name prefix to `search_store_` * add NEWS * add `mcptools` to Suggests * add `mcp_serve_store` to pkgdown
1 parent e86ad42 commit cfd6647

File tree

6 files changed

+181
-7
lines changed

6 files changed

+181
-7
lines changed

DESCRIPTION

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Imports:
3535
duckdb (>= 1.3.1),
3636
glue,
3737
httr2,
38+
jsonlite,
3839
methods,
3940
reticulate (>= 1.42.0),
4041
rlang (>= 1.1.0),
@@ -51,13 +52,14 @@ Suggests:
5152
gargle,
5253
knitr,
5354
lifecycle,
55+
mcptools,
5456
pandoc,
5557
paws.common,
5658
rmarkdown,
5759
shiny,
5860
stringr,
59-
tibble,
60-
testthat (>= 3.0.0)
61+
testthat (>= 3.0.0),
62+
tibble
6163
VignetteBuilder:
6264
knitr
6365
Config/Needs/website: tidyverse/tidytemplate, rmarkdown

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export(embed_openai)
1414
export(markdown_chunk)
1515
export(markdown_frame)
1616
export(markdown_segment)
17+
export(mcp_serve_store)
1718
export(ragnar_chunk)
1819
export(ragnar_chunk_segments)
1920
export(ragnar_chunks_view)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# ragnar (development version)
22

3+
* New function `mcp_serve_store()` which supports letting a local MCP client
4+
like Codex CLI or Claude Code search a `RagnarStore` (#123).
5+
6+
* The default tool name prefix registered by `ragnar_register_tool_retrive()` has
7+
changed from `rag_retrieve_from_` to `search_store_`.
8+
39
* Store Inspector updated with keyboard shortcuts, a draggable divider,
410
improved preview linkification and metadata display,
511
visual tweaks and general bug fixes (#120).

R/ellmer.R

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,38 @@ ragnar_register_tool_retrieve <- function(
3939
check_string(name, allow_null = TRUE)
4040
check_string(title, allow_null = TRUE)
4141

42-
name <- name %||% glue::glue("rag_retrieve_from_{store@name}")
42+
tool_def <- ragnar_tool_retrieve(
43+
store = store,
44+
store_description = store_description,
45+
...,
46+
name = name,
47+
title = title
48+
)
49+
50+
chat$register_tool(tool_def)
51+
invisible(chat)
52+
}
53+
54+
# Internal: build an ellmer tool for retrieving from a Ragnar store
55+
ragnar_tool_retrieve <- function(
56+
store,
57+
store_description = "the knowledge store",
58+
...,
59+
name = NULL,
60+
title = NULL
61+
) {
62+
rlang::check_installed("ellmer")
63+
64+
check_string(name, allow_null = TRUE)
65+
check_string(title, allow_null = TRUE)
66+
67+
name <- name %||% glue::glue("search_store_{store@name}")
4368
title <- title %||% store@title
4469

4570
previously_retrieved_chunk_ids <- integer()
71+
list(...) # force
4672

47-
tool_def <- ellmer::tool(
73+
ellmer::tool(
4874
function(text) {
4975
chunks <- ragnar_retrieve(
5076
store,
@@ -54,7 +80,14 @@ ragnar_register_tool_retrieve <- function(
5480
)
5581
previously_retrieved_chunk_ids <<-
5682
unique(unlist(c(chunks$chunk_id, previously_retrieved_chunk_ids)))
57-
chunks
83+
jsonlite::toJSON(
84+
chunks,
85+
pretty = TRUE,
86+
auto_unbox = TRUE,
87+
null = "null",
88+
na = "null",
89+
rownames = FALSE
90+
)
5891
},
5992
name = name,
6093
description = glue::glue(
@@ -71,7 +104,78 @@ ragnar_register_tool_retrieve <- function(
71104
open_world_hint = FALSE
72105
)
73106
)
107+
}
74108

75-
chat$register_tool(tool_def)
76-
invisible(chat)
109+
#' Serve a Ragnar store over MCP
110+
#'
111+
#' Launches an MCP server (via [mcptools::mcp_server()]) that exposes a
112+
#' retrieval tool backed by a Ragnar store. This lets MCP-enabled clients (e.g.,
113+
#' Codex CLI, Claude Code) call into your store to retrieve relevant
114+
#' excerpts.
115+
#'
116+
#' @param store A `RagnarStore` object or a file path to a Ragnar DuckDB store.
117+
#' If a character path is supplied, it is opened with
118+
#' [ragnar_store_connect()].
119+
#' @param store_description Optional string used in the tool description
120+
#' presented to clients.
121+
#' @inheritParams ragnar_register_tool_retrieve
122+
# ' @param ... Additional arguments forwarded to [ragnar_retrieve()].
123+
# ' @param name,title Optional identifiers for the tool. By default, derives from
124+
# ' `store@name` and `store@title` when available.
125+
#' @param extra_tools Optional additional tools (list of `ellmer::tool()`
126+
#' objects) to serve alongside the retrieval tool.
127+
#'
128+
#' @return This function blocks the current R process by running an MCP server.
129+
#' It is intended for non-interactive use. Called primarily for side-effects.
130+
#'
131+
#' @details
132+
#'
133+
#' To use this function with [Codex CLI](https://developers.openai.com/codex/cli/), add something like this
134+
#' to `~/.codex/config.toml`
135+
#'
136+
#' ```toml
137+
#' [mcp_servers.quartohelp]
138+
#' command = "Rscript"
139+
#' args = [
140+
#' "-e",
141+
#' "ragnar::mcp_serve_store(quartohelp:::quartohelp_ragnar_store(), top_k=10)"
142+
#' ]
143+
#' ```
144+
#'
145+
#' You can confirm the agent can search the ragnar store by inspecting the
146+
#' output from the `/mcp` command, or by asking it "What tools do you have
147+
#' available?".
148+
#'
149+
#'
150+
#' @export
151+
mcp_serve_store <- function(
152+
store,
153+
store_description = "the knowledge store",
154+
...,
155+
name = NULL,
156+
title = NULL,
157+
extra_tools = NULL
158+
) {
159+
rlang::check_installed("mcptools")
160+
rlang::check_installed("ellmer")
161+
162+
if (is.character(store)) {
163+
store <- ragnar_store_connect(store)
164+
}
165+
166+
retrieve_tool <- ragnar_tool_retrieve(
167+
store = store,
168+
store_description = store_description,
169+
...,
170+
name = name,
171+
title = title,
172+
as_json = TRUE
173+
)
174+
175+
tools <- c(list(retrieve_tool), extra_tools)
176+
if (rlang::is_installed("mcptools", version = "0.1.1.9001")) {
177+
mcptools::mcp_server(tools, include_session_tools = FALSE)
178+
} else {
179+
mcptools::mcp_server(tools)
180+
}
77181
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ reference:
5959
contents:
6060
- starts_with("ragnar_retrieve")
6161
- ragnar_register_tool_retrieve
62+
- mcp_serve_store
6263
- chunks_deoverlap

man/mcp_serve_store.Rd

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)