Building a Local Research Agent in LM Studio With Four MCP Servers
A local research agent in LM Studio with four MCP servers: filesystem, GitHub, SearXNG and Camofox, a trimmed tool schema, a plan prompt and a grounding demo.
This article is published in English.
Sources: How to Turn LM Studio Into a Local Research Agent With MCP Servers
A local model that can search, or read a file, or open a pull request is useful. A local model that can chain all of those inside one line of reasoning to answer a single question is a research assistant. Getting from the first to the second in LM Studio takes four MCP servers working side by side, a plan for keeping their combined tool schemas from swallowing the context window, and a nudge that makes a 9B-class model carry a multi-step task through instead of calling one tool and stopping.
The four servers are a filesystem server, a GitHub server, a self-hosted SearXNG instance for search, and a Camoufox-based browser (Camofox) for reading the pages that search turns up. The filesystem one is @modelcontextprotocol/server-filesystem, restricted to an allowed directory. GitHub is reached over OAuth at api.githubcopilot.com/mcp/. SearXNG and Camofox run in Docker and connect through mcp-searxng and camofox-mcp. Each works on its own. The interesting part is what happens when a single chat holds all of them.
One caution about framing before any wiring: everything here is meant for ordinary online research, such as competitive analysis, due diligence on a vendor or checking a claim from an article. The agent has no idea what is being researched and does not care. It searches, fetches and reads whatever it is pointed at.
The setup breaks into four stages: combining the servers, trimming the tool list, giving the model a plan, and running a grounding demo.
Stage 1: all four servers in one chat
LM Studio does not limit how many MCP servers a chat can use. It loads every one and hands the model each tool schema from each of them. That is the appeal of the setup, and, as the next stage shows, also the first thing that bites.
The filesystem and GitHub entries are configured as they would be for a standalone setup, and the SearXNG and Camofox entries likewise. The backing containers need to be up already, with their docker-compose.yml and searxng/settings.yml in place, because those files are not repeated here. What changes is that all four entries now live in one mcp.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/jes/research-workspace"
]
},
"github": {
"url": "https://api.githubcopilot.com/mcp/",
"auth": {
"CLIENT_ID": "<your Client ID>",
"CLIENT_SECRET": "<your client secret>"
}
},
"searxng": {
"command": "npx",
"args": ["-y", "mcp-searxng"],
"env": {
"SEARXNG_URL": "http://localhost:8080"
}
},
"camofox": {
"command": "npx",
"args": ["-y", "camofox-mcp"],
"env": {
"CAMOFOX_URL": "http://localhost:8765",
"CAMOFOX_API_KEY": "your-camofox-api-key",
"CAMOFOX_AUTH_MODE": "api_key"
}
}
}
}
Three details deserve attention.
- The
githubentry triggers its own OAuth step. LM Studio asks for it the first time the server tries to load. Because GitHub's endpoint does not support dynamic client registration, the flow uses a GitHub OAuth App that has to be registered by hand beforehand. There is no shortcut around that. - The allowed directory for the filesystem server is an arbitrary choice. A dedicated
research-workspacefolder works better than the home directory, mostly because it makes it obvious exactly what the model can touch. CAMOFOX_URLandSEARXNG_URLmust point at the Docker containers. If those are not running, the servers still load in LM Studio, but every tool call against them fails, which makes for a confusing twenty minutes the first time it happens.
When all four are enabled and the small green dots beside each server name in the MCP panel turn solid, the chat holds four filesystem tools, dozens of GitHub tools, four SearXNG tools and roughly forty-five Camofox tools. That is a lot of surface for one 9B model to reason over, and it is the problem the next stage addresses.
Stage 2: keeping the context budget alive
The GitHub tool count alone is a risk, as is Camofox's alone. Together, with SearXNG and filesystem tools stacked on top, the load gets serious.
Every tool's name, description and parameter schema is tokenized into the model's context before it has read a word of the actual question. On a small model with a modest window, that overhead can take a meaningful share of everything available.
The most direct fix is simply not to enable every tool from every server. For GitHub there is a better lever than toggling tools by hand in the MCP panel. The remote server accepts an X-MCP-Readonly header, which limits the entire connection to read-only tools before LM Studio sees the rest of the schema. With it, the github entry from the previous stage becomes:
{
"github": {
"url": "https://api.githubcopilot.com/mcp/",
"auth": {
"CLIENT_ID": "<your Client ID>",
"CLIENT_SECRET": "<your client secret>"
},
"headers": {
"X-MCP-Readonly": "true"
}
}
}
With the header on, the server advertises only reading tools: pull_request_read, issue_read, list_commits, get_file_contents, search_repositories and search_code. Anything able to delete, merge, push or create is simply absent. The restriction is enforced on GitHub's side, not by LM Studio hiding a button, so it is a true limit. It also means the schemas for the disabled write tools are never tokenized into context at all, which is the whole purpose of this stage.
Anyone wanting something narrower can use X-MCP-Tools, which takes an explicit comma-separated allowlist, for example search_code,search_repositories,get_file_contents,list_commits. That suits a case where the issue and pull-request tools will clearly never be needed.
A screenshot of the GitHub MCP in this state would show only get, read, list, scan and search tools, because the read-only flag was set to true, with the option to switch off further tools that are not needed and shrink the token cost more.
Camofox has no equivalent server-side flag, so its tools get switched off one at a time in the MCP panel. For a research-only chat, the candidates to switch off are delete_download, import_cookies and everything that manages profiles, save_profile and load_profile among them, since a research session has no reason to touch saved browser profiles.
The principle is to keep what the use case needs and disable the rest. For most servers that is a context budget decision and not a security one. The GitHub header is the exception, because GitHub's own server enforces it.
The demo in Stage 4 never needs GitHub write access, since the only GitHub action is reading one manifest, which means the read-only restriction can remain in force. If a later task does need writing, say to open an issue recording what the agent found, drop the header for that chat. Mixing read-only and write-capable GitHub connections in one session is not the way to handle it.
The context window itself needs attention too. A 4096-token minimum was a sensible floor when the schema on offer came from a single server. With four servers and a trimmed but still substantial tool list, 4096 is too small. Push the slider well up, possibly beyond 32k, depending on what the model and hardware can hold and how long the back-and-forth is expected to run. No single number is correct. It is a trade-off between reasoning room and spare VRAM, so watch the context-fullness percentage LM Studio displays under the chat input and adjust from there.
Stage 3: a plan the model can follow
A hand-coded agent has one advantage that LM Studio's chat UI does not provide automatically. In a script, the system prompt is a Python string under full control, and it can spell out the loop explicitly: search, choose a source, fetch it, then decide whether that answers the question or whether another search is needed.
It is better to be clear about what LM Studio really offers than to imply a hidden planning option exists. It does not. There is no separate planning setting.
What exists is a System Prompt field in the chat sidebar, under Model Parameters. That prompt can be saved together with temperature and context settings as a named Preset, so it need not be retyped each session. This is the same lever a hand-coded agent uses, only as a text box and not a string in code.
A system prompt along these lines works for a research chat:
You are a research assistant with tools for web search, page fetching,
file storage, and GitHub. When asked a factual question you cannot
answer with confidence from memory, follow this plan:
1. Search for the topic using searxng_web_search. Look at more than one
result before deciding a source is good.
2. Fetch the most promising source with camofox's navigate and
snapshot or extract_structured tools, not just the search snippet.
When using camofox always open a new tab before trying to navigate
to a page.
3. If the question needs more than one source to answer confidently,
search again and fetch again before answering.
4. If asked to save findings, write them to a markdown file in the
workspace using the filesystem tools, don't just describe them in chat.
5. State which sources you actually fetched, not just which ones you
found in search results.
Some notes on how it behaves:
- It is plain prose, with no state machine enforcing it. Instructions are what the model sees, and it generally follows their outline, with no guarantee, just as it would for a system prompt inside a script.
- Step 5 matters more than it appears. Without it, the model tends to answer from the search snippet alone and skip the fetch step, which defeats the reason for putting Camofox in the loop.
- Saving the result as a Preset lets it carry across chats without being pasted in again.
In the screenshot, the prompt sits under Model Parameters and the preset is saved as Research Chat. With the prompt and the trimmed tool list both in place, the model has the capability and something like a plan for using it. The remaining question is whether it actually does.
Stage 4: a demo that proves the tools were used
A good grounding test must force real tool use. Confident recall from memory cannot produce the right answer, so the model either fetches a real source or guesses. The task needs to be something it cannot have memorized, checkable independently, and general enough not to be about a specific person.
One that fits: point the agent at a repository that is maintained or relied upon, and have it follow one dependency's pinned version through to any security advisories filed against that exact version. The prompt below can be pasted into the chat after filling three placeholders:
I want you to check one dependency in a GitHub repository for known
security advisories. Do this:
1. In the repository <owner>/<repo>, read <manifest-file> using
get_file_contents.
2. From that file, find the exact pinned version of the dependency
"<dependency-name>".
3. Search the web for known security advisories filed against that
specific version of "<dependency-name>".
4. Fetch the actual advisory page(s) you find rather than relying on
the search snippet alone.
5. Write a short summary to dependency-risk.md in the workspace: the
dependency name, the pinned version, any advisories found (with
severity and a link), and a one-line recommendation.
The placeholders work like this:
<owner>/<repo>is any repository with read access. A personal project is as good as a public one that happens to be a dependency.<manifest-file>names the dependency file the project relies on, for instancego.mod,pyproject.toml,requirements.txtorpackage.json.<dependency-name>is a single dependency from that manifest, ideally one that is actually of interest.
Without a suitable repo to hand, juice-shop/juice-shop, the intentionally insecure training application from OWASP, can substitute. Its package.json carries enough past flagged dependencies that the search step usually turns up something real, and express-jwt and sanitize-html are both sensible starting picks. A filled-in version looks like this:
I want you to check one dependency in a GitHub repository for known
security advisories. Do this:
1. In the repository juice-shop/juice-shop, read package.json using
get_file_contents.
2. From that file, find the exact pinned version of the dependency
"express-jwt".
3. Search the web for known security advisories filed against that
specific version of "express-jwt".
4. Fetch the actual advisory page(s) you find rather than relying
on the search snippet alone.
5. Write a short summary to dependency-risk.md in the workspace:
the dependency name, the pinned version,
any advisories found (with severity and a link),
and a one-line recommendation.
Sending it should make the agent move through the five steps in order: a GitHub read, an external search, an external fetch and finally a file write, at whatever pace the hardware allows.
A complete run of this prompt used all four MCP servers and spent a total of 37803 tokens.
This prompt was chosen deliberately. The answer depends on the repo and what is pinned in it, and the CVE databases the searches hit are updated continuously, so no fixed result can be quoted in advance.
Checking the model's work takes two steps. Open the manifest and read the pinned version personally to verify what the GitHub call returned. After that, test the advisory claim against GitHub's own Security Advisories search or against NVD. If both match the agent's report, that counts as real grounding proof: the model actually read the manifest and actually fetched an advisory page, and did not answer from a vague memory that some library had a CVE once.
The demo also shows the Stage 2 read-only header is enough. The whole run is GitHub reads, outside search, outside fetch and a local file write, so nothing needs GitHub write access and the header can stay on. A single prompt exercises all four servers in one pass: GitHub reads the manifest, SearXNG searches for advisories, Camofox fetches the advisory pages and the filesystem saves the write-up. That makes it a quick way to confirm the whole setup is wired correctly before aiming it at real research.
Where it breaks
It is better to name the weak spots than to pretend the chain is reliable end to end.
Sometimes the model answers from the search snippet and skips the fetch, even with the system prompt telling it otherwise. This showed up more on short, simple-looking questions, where the model seemed to judge the snippet good enough. A follow-up message telling it to fetch the page usually fixes the problem. It is not that the model cannot, only that a 9B model gets some of these judgment calls wrong.
Occasionally, faced with dozens of GitHub tools it has never called, the model picks a plausible but wrong one, such as list_pull_requests when list_issues was meant. The pruning done in Stage 2 is the best defense. A shorter menu of look-alike choices leaves fewer chances to grab the wrong one.
None of this makes the agent a deterministic pipeline. Where determinism is required, a script is the right tool, and a hand-coded loop is closer to that. The payoff of wiring things into LM Studio instead is that the model adapts to whatever question is asked, and the price is that it sometimes adapts badly. Watching it work through a few research questions is the quickest way to learn where it can be trusted and where it needs a nudge.
The finished setup
The four stages leave a single LM Studio chat running four MCP servers. Its tool list has been pared down so the context still has space for thinking. A system prompt hands the 9B model a structure to follow where there was a blank page, and a grounding demo shows that real pages are being fetched instead of answers being recalled from memory. In concrete terms:
- Filesystem, GitHub, SearXNG and Camofox all enabled in a single chat.
- A pruned set of tools, plus a context window large enough for the combined schema instead of the one-server floor.
- A system prompt saved as a Preset, which gets the model planning a search, fetch and save loop on its own, using what LM Studio's UI actually exposes and not a config option that does not exist.
- A repeatable grounding demo, each part checkable independently, that touches all four servers in one prompt.
There is room to extend it. A second search backend alongside SearXNG would allow cross-checking results against each other. A summarization pass after every fetch would keep long pages from eating as much context on the way in. For anyone doing this regularly, scripting the Preset creation would start each new research chat correctly configured with no manual toggling.
One conclusion keeps coming back. After wiring the raw pieces together by hand, meaning filesystem, search, browsing and a code host, and watching where each one actually breaks, a heavier agent framework looks like what it is: the same moving parts tucked behind extra abstraction. Accepting that trade makes sense only for someone who already understands what lies underneath.