SmartGate MCP for Research and Decisions: A Personal Workflow
SmartGate MCP for research and decisions is one server entry in the assistant you already use. You ask the question; the gateway runs a fixed research chain - search, fetch, deduplicate, compress - and keeps the evidence in memory, so a decision can be re-derived later instead of re-guessed.
Short answer: SmartGate MCP for research and decisions is one server entry in the assistant you already use. You ask the question; the gateway runs a fixed research chain - search, fetch, deduplicate, compress - and keeps the evidence in memory, so a decision can be re-derived later instead of re-guessed.
Key takeaways
- One entry, no SDK. A
streamable-httpserver block in Claude Desktop or Cursor is the whole install; the free tier is 2M tokens a month at 120 requests/min per key. - The research chain is fixed, not improvised.
search(5 results) →fetch→dedup(0.85) →context_gate(0.4) runs the same way every time, which is what makes a result reproducible a month later. - Product questions stay on our domain, general questions do not. A question mentioning
SmartGate is biased to
site:smartgate.network, and a search that comes back empty is retried without that filter. - Research is searchable afterwards.
Memory.searchreturns 20 candidates at a 0.1 threshold and requires at least one identity filter - user, agent or run. - A budget read never blocks your work.
fetchBudgetCheckcaches its snapshot and returns null on failure; the cap itself is enforced on the call path. - Do this next: paste one server block, run one question through
smart_pipewith the research template, and keep the audit row as the baseline for your next decision.
buildClaudeDesktopMcpConfigJson: connect SmartGate MCP to Claude Desktop
The install is a configuration block, not a library. buildClaudeDesktopMcpConfigJson returns
the exact JSON a desktop host expects: one server named smartgate, type: "streamable-http", the MCP URL, and an authorization header built from your key placeholder
plus the platform agent id for Claude Desktop.
Two details are deliberate. The transport is streaming HTTP rather than a local process, so nothing runs on your machine beyond the assistant you already had. And the header carries the platform id, not just the key: every call lands in the audit trail tagged with the host that made it, which is how a task view can later separate "I asked this in Cursor" from "I asked this in Claude Desktop".
Paste it into the host's MCP configuration file, restart the host, and the tools appear as capabilities of the assistant you were already using. The free plan is enough to try the whole chain: 2M tokens a month, all of the primitives, 120 requests/min per key.
# lib/connect/mcp-config-templates.ts — source lines 76–96 (buildClaudeDesktopMcpConfigJson)
function buildClaudeDesktopMcpConfigJson(
mcpUrl: string,
apiKeyPlaceholder: string = API_KEY_PLACEHOLDER,
): string {
return JSON.stringify(
{
mcpServers: {
smartgate: {
type: "streamable-http",
url: mcpUrl,
headers: buildMcpAuthHeaders(
apiKeyPlaceholder,
PLATFORM_AGENT_ID["Claude Desktop"],
),
},
},
},
null,
2,
);
}
buildCursorMcpConfigJson: the same entry for Cursor
Cursor takes the same shape with a different agent id. buildCursorMcpConfigJson exists as its
own function rather than a parameter, because the two hosts differ in where the file lives and
how they name themselves in telemetry - and telemetry is the point: an audit row that says
"some client" is worth less than one that says which editor asked, when, and what it cost.
If you use both hosts, keep both entries. The key is shared, the budget is shared, and the audit trail stays one trail - a research run started in one editor can be found from the other, because memory and audit are gateway-side, not host-side.
# lib/connect/mcp-config-templates.ts — source lines 56–73 (buildCursorMcpConfigJson)
function buildCursorMcpConfigJson(
mcpUrl: string,
apiKeyPlaceholder: string = API_KEY_PLACEHOLDER,
): string {
return JSON.stringify(
{
mcpServers: {
smartgate: {
type: "streamable-http",
url: mcpUrl,
headers: buildMcpAuthHeaders(apiKeyPlaceholder, PLATFORM_AGENT_ID.Cursor),
},
},
},
null,
2,
);
}
smart_search: search the web from inside your own assistant
smart_search is the Research capability's first primitive: a query, a result ceiling between
1 and 50 (default 10, and the research template asks for 5), and nothing else to configure. It
resolves the search module from the gateway's registry and runs the call through the audit
wrapper, so the query text is recorded next to the result count.
That audit coupling is the difference between "I looked it up somewhere" and "here is the query I ran on Tuesday, and here are the five results it returned". When a decision is questioned three weeks later, the record is what lets you re-run the same question instead of defending a memory of it.
# backend/smartgate/api/mcp.py — source lines 128–148 (smart_search)
@server.tool(
name="smart_search",
description=TOOL_DESCRIPTIONS["smart_search"],
annotations=tool_annotations("smart_search"),
)
async def smart_search(
query: str = Field(description="Search query string."),
max_results: int = Field(
default=10,
description="Maximum number of results to return (1–50).",
),
) -> str:
_, registry = _app_state()
module = registry.get("search")
ctx = _tool_ctx()
return await _run_with_audit(
"search",
ctx,
module.process(ctx, query=query, max_results=max_results),
{"query": query},
)
_research_search_query: scope a research search to sites you trust
A research run on a product question should cite that product's own documentation, not a
forum post about it. _research_search_query implements exactly that bias and nothing more: if
the question mentions SmartGate, the query gains a site:smartgate.network filter; if you
already wrote a site: filter yourself, the function returns your query untouched.
The restraint matters more than the feature. There is no rewriting, no query expansion and no "AI-optimised" phrasing - a search you can predict is a search you can re-run. Everything in this article is meant to be checkable by you, and that starts with the query the gateway actually sent.
# backend/smartgate/core/pipeline.py — source lines 127–134 (_research_search_query)
def _research_search_query(query: str) -> str:
"""Bias research template toward on-domain SmartGate results."""
q = (query or "").strip()
if not q or "site:" in q.lower():
return q
if "smartgate" in q.lower():
return f"{q} site:smartgate.network"
return q
_run_search_with_research_fallback: retry a search that returned nothing
A site-scoped query can legitimately return nothing, and an empty first step would poison the
rest of the chain: nothing to fetch, nothing to deduplicate, nothing to compress.
_run_search_with_research_fallback handles that case for the research template only. If the
search succeeded but produced no usable results, it retries once with the site filter stripped,
and the millisecond timings of both attempts are summed into the step's reported duration.
Two properties are worth keeping in mind when you read a research result: the fallback is bounded (one retry, and only for the research template), and the timing you see is the real cost of both attempts, not a rounded average. A step that looks slow is usually a step that retried.
# backend/smartgate/core/pipeline.py — source lines 274–295 (_run_search_with_research_fallback)
async def _run_search_with_research_fallback(
self,
ctx: ToolContext,
module,
params: Dict[str, Any],
pipeline_template: str,
) -> tuple[ToolResult, float]:
t0 = time.perf_counter()
result = await module.process(ctx, **params)
elapsed = (time.perf_counter() - t0) * 1000.0
if pipeline_template != "research" or not result.success:
return result, elapsed
if _search_has_usable_results(result.data):
return result, elapsed
fallback_query = _research_search_fallback_query(params.get("query") or "")
if not fallback_query:
return result, elapsed
logger.info("research search empty, retry without site filter: %s", fallback_query)
t1 = time.perf_counter()
retry = await module.process(ctx, **{**params, "query": fallback_query})
elapsed += (time.perf_counter() - t1) * 1000.0
return retry, elapsed
deduplicate: remove repeated paragraphs from a research pile
Five search results about the same product contain the same three paragraphs. deduplicate
removes them in two passes: exact duplicates first, before any embedding work, and then
similarity above a threshold - 0.9 by default, 0.85 inside the research template. The return
value separates what survived from what was filtered, so the removal is reportable rather than
silent.
The early return matters at the small end: if every record was an exact duplicate, the call stops there instead of paying for embeddings it cannot use. For personal research the practical effect is that a brief reads like one document instead of five pages arguing with each other.
# backend/smartgate/modules/dedup/algorithm.py — source lines 152–184 (deduplicate)
def deduplicate(
self,
records: Sequence[Record],
threshold: float = 0.9,
) -> DeduplicationResult:
"""
Perform deduplication against the fitted index.
This method assumes you have already fit on a reference dataset (e.g., a train set) with from_records.
It will remove any items from 'records' that are similar above a certain threshold
to any item in the fitted dataset.
:param records: A new set of records (e.g., test set) to deduplicate against the fitted dataset.
:param threshold: Similarity threshold for deduplication.
:return: A deduplicated list of records.
"""
dict_records = self._validate_if_strings(records)
# Remove exact duplicates before embedding
dict_records, exact_duplicates = remove_exact_duplicates(
records=dict_records, columns=self.columns, reference_records=self.index.items
)
duplicate_records = []
for record, duplicates in exact_duplicates:
duplicated_with_score = add_scores_to_records(duplicates)
duplicate_record = DuplicateRecord(record=record, duplicates=duplicated_with_score, exact=True)
duplicate_records.append(duplicate_record)
# If no records are left after removing exact duplicates, return early
if not dict_records:
return DeduplicationResult(
selected=[], filtered=duplicate_records, threshold=threshold, columns=self.columns
)
PromptCompressor.control_context_budget: keep a long brief inside the budget
Fetched pages are long; your context window is not. control_context_budget compresses the
context down to a budget you set, and its two parameters are the ones to understand. The budget
is expressed relative to the request (+100 means ten percent over the target, applied as an
offset on the target token count), and strict_preserve_uncompressed guarantees that any
segment already marked as not-compressible is kept whole rather than summarised away.
Used inside the research template the ratio is 0.4: roughly 40% of the fetched evidence reaches the model, and the rest stays in the audit trail. If a decision later looks under-informed, the first thing to check is not the model - it is what the compression step kept.
# backend/smartgate/modules/context_gate/algorithm.py — source lines 1190–1224 (PromptCompressor.control_context_budget)
def control_context_budget(
self,
context: List[str],
context_tokens_length: List[int],
target_token: float,
force_context_ids: List[int] = None,
force_context_number: int = None,
question: str = "",
condition_in_question: str = "none",
reorder_context: str = "original",
dynamic_context_compression_ratio: float = 0.0,
rank_method: str = "longllmlingua",
context_budget: str = "+100",
context_segs: List[List[str]] = None,
context_segs_rate: List[List[float]] = None,
context_segs_compress: List[List[bool]] = None,
strict_preserve_uncompressed: bool = True,
):
demostrations_sort = self.get_rank_results(
context,
question,
rank_method,
condition_in_question,
context_tokens_length,
)
if target_token < 0:
target_token = 100
target_token = eval("target_token" + context_budget)
res = []
used = force_context_ids if force_context_ids is not None else []
if context_segs is not None and strict_preserve_uncompressed:
for idx, _ in enumerate(context):
if False in context_segs_compress[idx] and idx not in used:
used.append(idx)
Memory.search: search earlier research before you decide again
The Memory capability answers a different question from search: not "what does the web say" but
"what did I already conclude". Memory.search takes a query and returns up to 20 candidates by
default at a similarity threshold of 0.1, with reranking off unless you ask for it. Every call
must carry at least one identity filter - user, agent or run - so one person's research never
surfaces in another person's results.
The remember pipeline template is the write side of the same idea: search first, then store
what mattered with memory in add mode. Retention follows your plan (7 days on Free, 30 on Pro,
90 on Teams), which is the honest limit of "personal knowledge system": it is a working memory
with a retention policy, not an archive you should stop curating.
# backend/smartgate/modules/memory/algorithm.py — source lines 1126–1135 (Memory.search)
def search(
self,
query: str,
*,
top_k: int = 20,
filters: Optional[Dict[str, Any]] = None,
threshold: float = 0.1,
rerank: bool = False,
**kwargs,
):
fetchBudgetCheck: check spend before an expensive research step
Cost control for a personal workflow has to be invisible or it will not be used.
fetchBudgetCheck reads the team's budget snapshot under a cache key, and the cache TTL is the
reason a dashboard can show a limit without making an API call per render. The request is marked
source: "dashboard_read" with audit: false, so a read never pollutes the audit trail.
Note the failure behaviour: the function catches and returns null. A budget read that fails must not block a page from loading - that is the opposite of the enforcement path, where the call itself is capped. Read the two separately: the snapshot is for humans, the cap is for the request.
# lib/budget/fetch-budget-check.ts — source lines 22–60 (fetchBudgetCheck)
async function fetchBudgetCheck(
ctx: SmartgateCallContext,
): Promise<BudgetCheckSnapshot | null> {
const key = budgetSnapshotKey(ctx.teamId);
const cached = getCached<BudgetCheckSnapshot>(key);
if (cached) return cached;
try {
const result = await callSmartgateApi<BudgetCheckSnapshot & { exceeded?: boolean }>(
"budget/check",
{
action: "check",
team_id: ctx.teamId,
monthly_limit: ctx.entitlementLimit,
entitlement_limit: ctx.entitlementLimit,
client_budget: ctx.clientBudget,
audit: false,
source: "dashboard_read",
},
ctx,
);
const data = result.data;
if (!data) return null;
const snapshot: BudgetCheckSnapshot = {
used: data.used,
limit: data.limit,
remaining: data.remaining,
allowed: data.allowed,
binding_constraint: data.binding_constraint,
};
setCached(key, snapshot, budgetCheckCacheTtlSec());
return snapshot;
} catch {
return null;
}
}
smart_pipe: run a saved research pipeline in one call
smart_pipe is the Pipeline capability, and for a non-developer it is the most useful surface
in the gateway. Instead of wiring four tool calls by hand, you call one tool with a template
name: research, read or remember. The arguments are deliberately few - a query, a url, or
a text seed - because the template already knows which steps follow.
Custom pipelines are available when a template is not enough, as a list of steps each naming a tool and its parameters. The reason to start with a template is not simplicity for its own sake: a template is a chain you can compare against yesterday's chain. Once you have a research question you ask weekly, one call replaces four, and the audit row shows the sequence either way.
# backend/smartgate/api/mcp.py — source lines 302–335 (smart_pipe)
@server.tool(
name="smart_pipe",
description=TOOL_DESCRIPTIONS["smart_pipe"],
annotations=ToolAnnotations(
title="Pipeline orchestrator",
readOnlyHint=False,
),
)
async def smart_pipe(
template: str = Field(
default="",
description="Built-in template: research, read, or remember. Omit when using steps.",
),
steps: list[dict[str, Any]] | None = Field(
default=None,
description="Custom steps; each step has tool and params (legacy alias: args).",
),
query: str = Field(
default="",
description="Search query for research/remember templates (wired to search step).",
),
url: str = Field(
default="",
description="URL for read/research templates (wired to fetch when search is skipped).",
),
text: str = Field(
default="",
description="Optional text seed for context_gate when not produced by a prior fetch.",
),
) -> str:
from smartgate.core.pipeline import PipelineEngine
_, registry = _app_state()
engine = PipelineEngine(registry)
pipelineMarkdown: turn a research run into a readable brief
Text that comes out of a pipeline has to survive rendering before anyone reads it.
pipelineMarkdown takes the source string through remark with GFM, converts it to HTML, and
runs the result through rehype-sanitize before it reaches a page. The sanitizer is the part
that matters: fetched material is third-party content, and third-party content does not get to
inject markup into your workspace.
The practical consequence for a personal workflow is that a research brief can be pasted somewhere shared without a second cleaning pass - tables and lists survive, embedded scripts do not. If your own notes render as a wall of text, the fence or the table syntax is what to check first, not the host.
# lib/pseo/markdown.ts — source lines 18–27 (pipelineMarkdown)
async function pipelineMarkdown(source: string): Promise<string> {
const file = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeSanitize, sanitizeSchema)
.use(rehypeStringify)
.process(source);
return String(file);
}
PipelineEngine: run the four step research pipeline end to end
PipelineEngine.run is the loop behind every template. It takes either a template name or an
explicit step list, builds a pipeline context, and then walks the steps in order: resolve the
tool name, resolve that step's parameters against the run inputs, execute, and record the step
name and its timing. context_gate steps get an extra guarantee - if compression fails or
returns nothing, the engine falls back to the text already gathered in the pipeline instead of
handing the model an empty context.
That is the whole design in one paragraph, and it is deliberately boring: a fixed sequence with per-step timing and explicit fallbacks. Boring is what lets you say "run the research template on this question" and get an answer you can compare with the one you got last week.
# backend/smartgate/core/pipeline.py — source lines 341–372 (PipelineEngine.run)
async def run(
self,
ctx: ToolContext,
template: str = "",
steps: List[Dict] = None,
inputs: Optional[Dict[str, Any]] = None,
) -> Dict:
pipeline_ctx = PipelineContext()
ctx = ToolContext(pipeline_context=pipeline_ctx)
run_inputs = inputs or {}
results = []
if template and template in PIPELINE_TEMPLATES:
steps = PIPELINE_TEMPLATES[template]
if not steps:
return {"error": "No steps or template provided", "results": [], "pipeline_steps": []}
offset_ms = 0.0
pipeline_steps: List[Dict[str, Any]] = []
for i, step in enumerate(steps):
tool_name = step["tool"]
module_name = resolve_pipeline_tool_name(tool_name)
params = resolve_pipeline_step_params(
module_name,
_step_params(step),
pipeline_ctx,
run_inputs,
pipeline_template=template,
)
step_name = step.get("name", f"step_{i}")
How SmartGate compares
| What it answers | Where the evidence ends up | What you pay | |
|---|---|---|---|
| Private plugins inside one assistant | one vendor's model, one vendor's tools | that chat's transcript | a seat price |
| A research app | a summarised answer | their index, not yours | a subscription |
| A model gateway (LiteLLM, Portkey, Kong) | which model serves the call | the provider invoice | a platform fee |
| SmartGate | research, context, memory and control as capabilities | your audit rows and your memory, both queryable | a platform fee, plus a share only after measured savings |
The distinction worth testing is the last column and the third: a gateway that routes models tells you what you spent. An intelligence layer tells you what you asked, what came back, what was kept, and what it cost - which is the part a decision actually needs.
How to get started
- Sign in at smartgate.network/login - the free plan needs no card and includes all seven primitives.
- Skim the RAG architecture guide if your research questions need retrieval rather than plain fetch — it covers chunking, dedup and compression on the same MCP surface.
- Copy the server block for your host from /docs/connect and paste it into your assistant's MCP configuration.
- Run one question you have researched before, through
smart_pipewith theresearchtemplate, and compare the brief with your old notes. - Keep it once the comparison holds; the platform fee is a flat plan, and the savings share only starts after measured savings clear $15 (pricing).
Frequently Asked Questions
Limitations and what this does not do
- The research template is four fixed steps, not an agent. It will not decide to search again because the first answer felt thin; custom pipelines are how you add that step.
- Similarity thresholds drop things. At 0.85 a genuinely different paragraph that happens to share a lot of vocabulary can be filtered out; the return value tells you what was removed, so read it when the brief looks shorter than expected.
- Compression is lossy by design. A 0.4 ratio means most of the fetched text never reaches the model. Mark what must survive as not-compressible, or raise the ratio for that run.
- Memory is working memory with a retention policy, not an archive. Long-lived knowledge needs your own export.
- A failed budget read returns null. That keeps a dashboard usable, but it also means the snapshot can be missing exactly when something is wrong; the cap on the call path is the guarantee, not the snapshot.
- It is not a model router or a search engine. Pair it with the provider you already use; the value here is the evidence trail around the call, not a different model.
Sources
- Model Context Protocol - tools and server specification: https://modelcontextprotocol.io/specification/latest
- Model Context Protocol - connecting a local server to a host: https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers
- Cursor - MCP servers and configuration: https://cursor.com/docs/mcp
- LongLLMLingua - prompt compression with a target budget: https://arxiv.org/abs/2403.19093
- rehype-sanitize - the HTML sanitiser used before pipeline text renders: https://github.com/rehypejs/rehype-sanitize
- SmartGate - documentation, pricing and contact: https://smartgate.network/docs · https://smartgate.network/pricing · https://smartgate.network/contact
Method note
The code in this article is not transcribed. Each block was cut directly out of the slice body
returned by the SmartGate slice API and re-asserted byte-for-byte as a substring of that body
before publication; the first line inside every fence records the file and the exact source
lines. Symbols were pinned by whole-name containment (rule A level 2) and confirmed by the
service's slot-proof endpoint before being written into the prose - 12 of 12 planned sections
pinned, no abstentions. Every value quoted in the prose (five results, 0.85, 0.4, 20, 0.1, the
cache TTL, the +100 budget offset) is read from the same file shown above it.
Slice provenance
| # | SERP keyword | Symbol | File | Source lines | How it was pinned | sha256(12) |
|---|---|---|---|---|---|---|
| 1 | buildClaudeDesktopMcpConfigJson connect SmartGate MCP to Claude Desktop | buildClaudeDesktopMcpConfigJson |
lib/connect/mcp-config-templates.ts |
76–96 | rule A L2 → slot-proof | 8f2a75a4ac53 |
| 2 | buildCursorMcpConfigJson connect SmartGate MCP to Cursor | buildCursorMcpConfigJson |
lib/connect/mcp-config-templates.ts |
56–73 | rule A L2 → slot-proof | 5ac3816f50b3 |
| 3 | smart_search search the web from inside your own assistant | smart_search |
backend/smartgate/api/mcp.py |
128–148 | rule A L2 → slot-proof | 12366ba0d241 |
| 4 | _research_search_query scope a research search to sites you trust | _research_search_query |
backend/smartgate/core/pipeline.py |
127–134 | rule A L2 → slot-proof | 93891aca3106 |
| 5 | _run_search_with_research_fallback retry a search that returned nothing | _run_search_with_research_fallback |
backend/smartgate/core/pipeline.py |
274–295 | rule A L2 → slot-proof | 2bc7aeecffeb |
| 6 | deduplicate remove repeated paragraphs from a research pile | deduplicate |
backend/smartgate/modules/dedup/algorithm.py |
152–184 | rule A L2 → slot-proof | b1e0bd80f82c |
| 7 | PromptCompressor.control_context_budget keep a long brief inside the budget | PromptCompressor.control_context_budget |
backend/smartgate/modules/context_gate/algorithm.py |
1190–1224 | rule A L2 → slot-proof | cf179b5bbe39 |
| 8 | Memory.search search earlier research before you decide again | Memory.search |
backend/smartgate/modules/memory/algorithm.py |
1126–1135 | rule A L2 → slot-proof | aa50fd88cf6d |
| 9 | fetchBudgetCheck check spend before an expensive research step | fetchBudgetCheck |
lib/budget/fetch-budget-check.ts |
22–60 | rule A L2 → slot-proof | 26215c9f6e15 |
| 10 | smart_pipe run a saved research pipeline in one call | smart_pipe |
backend/smartgate/api/mcp.py |
302–335 | rule A L2 → slot-proof | 91ee6d5bb70b |
| 11 | pipelineMarkdown turn a research run into a readable brief | pipelineMarkdown |
lib/pseo/markdown.ts |
18–27 | rule A L2 → slot-proof | 52ce6db05b40 |
| 12 | PipelineEngine run the four step research pipeline end to end | PipelineEngine |
backend/smartgate/core/pipeline.py |
341–372 | rule A L2 → slot-proof | 5f7a984d6b6d |
Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before publication. 12 of 12 sections pinned, 0 abstentions, 0 misses.