Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ server = [
# Scheduling
"apscheduler>=3.10.0,<4.0.0",
"opencite>=0.5.3",
# Model Context Protocol client, for tools served by an MCP host
# (src/tools/mcp_client.py). 2.x specifically: the 2026-07-28 protocol
# revision does not exist in 1.x, and langchain-mcp-adapters -- the obvious
# alternative -- pins mcp<2.0.0, which is why this wraps the SDK directly.
"mcp>=2.2.0",
]

observability = [
Expand Down
36 changes: 36 additions & 0 deletions src/assistants/community.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Recent GitHub activity listing (if repos configured)
- Paper search (if citations configured)
- Python plugin tools (if extensions configured)
- MCP server tools (if extensions configure an MCP server)
"""

import importlib
Expand Down Expand Up @@ -150,6 +151,7 @@ class CommunityAssistant(ToolAgent):
- Recent GitHub activity listing (if repos configured)
- Paper search (if citations configured)
- Python plugin tools (if extensions configured)
- MCP server tools (if extensions configure an MCP server)

Args:
model: The language model to use.
Expand Down Expand Up @@ -204,6 +206,13 @@ def __init__(
plugin_tools = self._load_plugin_tools(config)
tools.extend(plugin_tools)

# Load tools served by configured MCP servers. Same contract as the
# plugin loader above: log and continue on failure, never raise out of
# this constructor. An assistant that cannot start because someone
# else's host is down is worse than one missing a few tools.
mcp_tools = self._load_mcp_tools(config)
tools.extend(mcp_tools)

# Generate system prompt
system_prompt = self._build_system_prompt(config, additional_instructions)

Expand Down Expand Up @@ -301,6 +310,33 @@ def _load_plugin_tools(self, config: CommunityConfig) -> list[BaseTool]:

return all_tools

def _load_mcp_tools(self, config: CommunityConfig) -> list[BaseTool]:
"""Load tools from configured Model Context Protocol (MCP) servers.

Deliberately shaped exactly like `_load_plugin_tools`: a failure is
logged and skipped, and this never raises. `discover_mcp_tools` already
swallows per-server failures, so the try here covers the import itself --
`mcp` lives in the `server` extra, and a CLI-only install must not break
on it.
"""
all_tools: list[BaseTool] = []

if not config.extensions or not config.extensions.mcp_servers:
return all_tools

try:
from src.tools.mcp_client import discover_mcp_tools
except ImportError as e:
logger.error("MCP support unavailable (install the server extra): %s", e)
return all_tools

for server in config.extensions.mcp_servers:
server_tools = discover_mcp_tools(server)
logger.info("Loaded %d tools from MCP server %s", len(server_tools), server.name)
all_tools.extend(server_tools)

return all_tools

def _format_preloaded_section(self) -> str:
"""Format preloaded documents for the system prompt."""
if not self._preloaded_content:
Expand Down
23 changes: 12 additions & 11 deletions src/assistants/nemar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
Self-contained assistant module for discovering and exploring BIDS-formatted
EEG, MEG, and iEEG datasets hosted on NEMAR (nemar.org).

This module provides specialized Python tools for NEMAR that cannot be
auto-generated from YAML:
- search_nemar_datasets: Search and filter datasets by text, modality, task, etc.
- get_nemar_dataset_details: Get full metadata for a specific dataset
This module carries no Python tools. Its dataset tools come from NEMAR's own MCP
server (`https://mcp.nemar.org/mcp`), configured under `extensions.mcp_servers`
in `config.yaml` and loaded by `src/tools/mcp_client.py`.

That replaced two hand-written tools, `search_nemar_datasets` and
`get_nemar_dataset_details`, which called
`nemar.org/api/dataexplorer/datapipeline/...`. That endpoint returns 404: the
legacy dataexplorer site is gone and its URLs now redirect to
`nemar.org/dataset/<id>`. So the tools had been non-functional, and the MCP
server replaces them rather than supplementing them -- `search_datasets` and
`describe_dataset` map onto the two of them almost exactly, with four more tools
for what is inside a dataset.

All other configuration (system prompt, CORS, budget) is in config.yaml.
"""

from .tools import get_nemar_dataset_details, search_nemar_datasets

__all__ = [
"search_nemar_datasets",
"get_nemar_dataset_details",
]
124 changes: 75 additions & 49 deletions src/assistants/nemar/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,51 +82,76 @@ system_prompt: |
- When users describe research interests, search for relevant datasets
- Use the search tool to browse datasets by characteristics (modality, task, HED annotations, etc.)
- Use the details tool to get comprehensive information about specific datasets

## Using the search_nemar_datasets Tool

This is your primary discovery tool. Use it to help users find datasets matching their needs.

**Search strategies:**
- Text search: Search across dataset names, tasks, README content, and authors
- Modality filter: Find datasets with specific recording types (EEG, MEG, iEEG, MRI)
- Task filter: Find datasets with specific experimental paradigms
- HED filter: Find datasets with HED annotations for structured event description
- Participant range: Find datasets with sufficient subject counts
- Combine multiple filters to narrow results

**Important guidelines:**
- Always search when users ask "find datasets", "show me datasets", "are there datasets with..."
- Search returns compact summaries (ID, name, modality, tasks, participant count, size)
- Follow up with get_nemar_dataset_details for datasets the user is interested in
- Present search results as a numbered or bulleted list with key characteristics

## Using the get_nemar_dataset_details Tool

Use this to retrieve comprehensive information about a specific dataset.

**When to use:**
- User asks "tell me more about ds00XXXX"
- User wants citation information, full README, licensing details
- After search results, when user shows interest in a specific dataset

**Information to highlight:**
- OpenNeuro link: https://openneuro.org/datasets/{dataset_id}
- NEMAR link: https://nemar.org/dataexplorer/detail?dataset_id={dataset_id}
- Citation: Use the DatasetDOI field
- Licensing: Mention the License field (typically CC0)
- Data characteristics: Participants, sessions, modalities, tasks
- HED annotations: If present, highlight this for users interested in standardized event descriptions
- Prefer the cheapest tool that answers the question; the ladder below is ordered

## The NEMAR tools, cheapest first

Your dataset tools come from NEMAR's MCP server and are named with a `nemar_`
prefix. They form a deliberate cost ladder: each step down reads more, so walk it
rather than jumping to the bottom. `nemar_describe_dataset` returns a `cost_hint`
naming the next cheapest tool.

1. **`nemar_search_datasets`** - which datasets match. Filters: free text, modality,
task, HED annotations, participant counts, and `has_zarr` for datasets with a
streaming copy. Start here for any "find me datasets" question.
2. **`nemar_describe_dataset`** - what one dataset is, plus a ready-made citation
string and its DOI and license. Use it as the follow-up when a user picks one.
3. **`nemar_list_recordings`** - which recordings and channel groups a dataset has,
with channel counts, durations, and sampling rates.
4. **`nemar_get_events`** - the event table for one recording: onsets, trial types,
HED strings, and exact sample indices.
5. **`nemar_render_overview`** - a PNG overview of a recording, for "what does this
look like".
6. **`nemar_read_window`** - how to read a specific time window. By default it
returns a *recipe*: the array URL, the sample range, and the dequantization rule,
which is what a user should be given so they can read the data themselves. It can
decode a tiny window inline if you pass `taste: true` with an explicit channel
list, but that is for illustrating a signal, never for analysis.

**Dataset identifiers** are `nm` or `on` followed by six digits, for example
`nm000329`. They are not OpenNeuro `ds` accessions.

## Presenting datasets

- Link to `https://nemar.org/dataset/{dataset_id}`.
- Cite using the `citation` field from `nemar_describe_dataset` verbatim; it is
already formatted and carries the version and DOI. Do not compose your own.
- Mention the license, and say plainly when it restricts reuse.
- Search results are compact summaries; get details for the one or two the user
actually cares about rather than describing everything.

## Being honest about the streaming copy

When you report anything read through `nemar_list_recordings`, `nemar_get_events`,
`nemar_render_overview` or `nemar_read_window`, those answers carry an `envelope`
with provenance. Three fields you must not paper over:

- **`lossy` is always true.** The streaming copy is quantized and rate-capped
relative to the original recording. If `effective_rate_hz` is lower than
`source_rate_hz`, say so: the user is looking at a downsampled view. Anyone who
needs the original samples should download the BIDS files.
- **`zarr_verify_status` may be `null`**, which means the fidelity sweep has not
reached that conversion yet. That is "not checked", not "wrong" - do not describe
it as a problem, and do not imply it has been verified either.
- **`filled_ranges`** on a decoded window lists spans that had no stored data and
were filled in. If it is non-empty, say which part of the window is not real
signal.

If a tool declines a request, read what it says. These refusals are specific and
usually name a workaround - a smaller window, or a public URL to read directly -
and relaying that is more useful than reporting a failure.

## Dataset Discovery Workflow

**Typical interaction pattern:**
1. User describes research interest: "I need EEG datasets for attention tasks"
2. CALL search_nemar_datasets(query="attention", modality_filter="EEG")
3. Present relevant datasets as a list
4. User asks about specific dataset: "Tell me more about #3"
5. CALL get_nemar_dataset_details(dataset_id="ds00XXXX")
6. Present comprehensive information with OpenNeuro link and citation
1. User describes a research interest: "I need EEG datasets for attention tasks"
2. CALL `nemar_search_datasets(query="attention", modality_filter="EEG")`
3. Present the relevant datasets as a list
4. User asks about a specific one: "Tell me more about #3"
5. CALL `nemar_describe_dataset(dataset_id="nm000329")`
6. Present the details with the `nemar.org/dataset/{id}` link and the citation
7. If they want to know what is inside: `nemar_list_recordings`, then
`nemar_get_events` or `nemar_render_overview` for one recording

## BIDS and HED Context

Expand Down Expand Up @@ -162,8 +187,8 @@ system_prompt: |
# Documentation sources (NEMAR is a data portal, so minimal docs)
documentation:
- title: NEMAR Data Explorer
url: https://nemar.org/dataexplorer
source_url: https://nemar.org/dataexplorer
url: https://nemar.org/dataset
source_url: https://nemar.org/dataset
preload: false
category: reference
description: NEMAR dataset browser and exploration interface.
Expand All @@ -182,10 +207,11 @@ documentation:
category: reference
description: Brain Imaging Data Structure specification that all NEMAR datasets follow.

# Custom tools for NEMAR API interaction
# Dataset tools come from NEMAR's MCP server (https://mcp.nemar.org/mcp), which is
# anonymous and needs no credentials. It replaced two python_plugins tools that
# called nemar.org/api/dataexplorer/datapipeline/... -- that endpoint returns 404,
# because the legacy dataexplorer site is gone, so those tools had been dead.
extensions:
python_plugins:
- module: src.assistants.nemar.tools
tools:
- search_nemar_datasets
- get_nemar_dataset_details
mcp_servers:
- name: nemar
url: https://mcp.nemar.org/mcp
Loading
Loading