agentgraph-server 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentgraph/__init__.py +1 -0
- agentgraph/auth/__init__.py +0 -0
- agentgraph/auth/credentials.py +224 -0
- agentgraph/backends/__init__.py +50 -0
- agentgraph/backends/sqlite/__init__.py +1 -0
- agentgraph/backends/sqlite/backend.py +1471 -0
- agentgraph/backends/sqlite/vector.py +142 -0
- agentgraph/cli.py +721 -0
- agentgraph/cli_query.py +519 -0
- agentgraph/config.py +90 -0
- agentgraph/connectors/__init__.py +0 -0
- agentgraph/connectors/base.py +455 -0
- agentgraph/connectors/registry.py +78 -0
- agentgraph/connectors/status.py +244 -0
- agentgraph/core/__init__.py +0 -0
- agentgraph/core/context.py +26 -0
- agentgraph/core/runtime.py +36 -0
- agentgraph/core/storage.py +240 -0
- agentgraph/graph/__init__.py +1 -0
- agentgraph/graph/bookmark.py +87 -0
- agentgraph/graph/delete.py +17 -0
- agentgraph/graph/download.py +35 -0
- agentgraph/graph/embeddings.py +58 -0
- agentgraph/graph/fetch.py +53 -0
- agentgraph/graph/gc.py +26 -0
- agentgraph/graph/link.py +63 -0
- agentgraph/graph/person.py +40 -0
- agentgraph/graph/query.py +244 -0
- agentgraph/graph/upsert.py +49 -0
- agentgraph/logging.py +78 -0
- agentgraph/mcp/__init__.py +0 -0
- agentgraph/mcp/server.py +811 -0
- agentgraph/perf.py +43 -0
- agentgraph/server/__init__.py +0 -0
- agentgraph/server/app.py +133 -0
- agentgraph/server/cli_api.py +708 -0
- agentgraph/server/dwell.py +79 -0
- agentgraph/server/graph_api.py +46 -0
- agentgraph/server/router.py +47 -0
- agentgraph/server/sync.py +247 -0
- agentgraph/skills.py +93 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/graph/SKILL.md +159 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/slack-auth/SKILL.md +92 -0
- agentgraph_server-0.5.0.dist-info/METADATA +286 -0
- agentgraph_server-0.5.0.dist-info/RECORD +49 -0
- agentgraph_server-0.5.0.dist-info/WHEEL +5 -0
- agentgraph_server-0.5.0.dist-info/entry_points.txt +2 -0
- agentgraph_server-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentgraph_server-0.5.0.dist-info/top_level.txt +1 -0
agentgraph/mcp/server.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""AgentGraph MCP server.
|
|
2
|
+
|
|
3
|
+
Exposes the knowledge graph as MCP tools so AI agents can search,
|
|
4
|
+
retrieve, and traverse entities directly.
|
|
5
|
+
|
|
6
|
+
Run via:
|
|
7
|
+
agentgraph mcp
|
|
8
|
+
or via the `agentgraph/mcp` stdio transport understood by Claude Desktop
|
|
9
|
+
and other MCP clients.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from mcp.server.fastmcp import FastMCP
|
|
20
|
+
|
|
21
|
+
from agentgraph.core.context import get_backend
|
|
22
|
+
from agentgraph.graph.query import (
|
|
23
|
+
get_edges,
|
|
24
|
+
get_entity,
|
|
25
|
+
query_by_filter,
|
|
26
|
+
search_entities,
|
|
27
|
+
traverse_graph,
|
|
28
|
+
)
|
|
29
|
+
from agentgraph.perf import timed
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
mcp = FastMCP("AgentGraph")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _truncate_content(entity: dict[str, Any], limit: int = 500) -> None:
|
|
36
|
+
content = entity.get("content")
|
|
37
|
+
if isinstance(content, str) and len(content) > limit:
|
|
38
|
+
entity["content"] = content[:limit] + "…"
|
|
39
|
+
entity["content_truncated"] = True
|
|
40
|
+
else:
|
|
41
|
+
entity["content_truncated"] = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# list_connectors — connector discovery for agents
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
async def list_connectors_tool(verify: bool = False) -> str:
|
|
51
|
+
"""
|
|
52
|
+
List all installed connectors and their capabilities.
|
|
53
|
+
|
|
54
|
+
Call this first to understand which data sources are available and
|
|
55
|
+
which platform values are valid for the platform= parameter in other
|
|
56
|
+
tools (search_entities_tool, query_by_filter_tool, fetch_entity_tool).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
JSON array of connector objects, each with:
|
|
60
|
+
- source: platform name to pass as the platform= argument
|
|
61
|
+
- description: what this connector ingests
|
|
62
|
+
- auth_provider: shared auth provider key (e.g. "google"), or null
|
|
63
|
+
for connectors that do not use credentials
|
|
64
|
+
- auth_status: "ok" | "missing" | "invalid", or null when no auth is used
|
|
65
|
+
- auth_detail: aggregate auth summary or error message; null if missing
|
|
66
|
+
- auth_verified: true when credentials were live-checked with provider APIs
|
|
67
|
+
- shared_auth: true when multiple connectors share the same auth provider
|
|
68
|
+
- account_count: number of authenticated accounts for that provider
|
|
69
|
+
- url_patterns: URL patterns this connector recognises
|
|
70
|
+
- polls: true if this connector has its own background poll
|
|
71
|
+
- poll_interval_seconds: direct poll interval, or null
|
|
72
|
+
- poll_delegates: connector sources refreshed by this connector's poll
|
|
73
|
+
- polled_by: connector sources whose poll refreshes this connector
|
|
74
|
+
- sync: human-readable sync summary
|
|
75
|
+
- last_synced_at: latest entity sync timestamp for this connector source, or null
|
|
76
|
+
- last_sync: human-readable last-sync label
|
|
77
|
+
"""
|
|
78
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
79
|
+
from agentgraph.connectors.status import connector_status_items
|
|
80
|
+
|
|
81
|
+
bootstrap()
|
|
82
|
+
all_connectors = get_all_connectors()
|
|
83
|
+
result = await connector_status_items(all_connectors, get_backend(), verify=verify)
|
|
84
|
+
return json.dumps(result)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@mcp.tool()
|
|
88
|
+
async def list_auth_providers_tool(verify: bool = False) -> str:
|
|
89
|
+
"""
|
|
90
|
+
List credential-backed authentication providers and their current account/auth state.
|
|
91
|
+
|
|
92
|
+
Connectors that require only configuration, or no setup at all, are omitted.
|
|
93
|
+
Use list_connectors_tool to inspect all installed connectors.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
JSON array of auth provider objects, each with:
|
|
97
|
+
- provider: auth provider key such as "google" or "slack"
|
|
98
|
+
- description: provider summary
|
|
99
|
+
- connectors: connector sources that use this provider
|
|
100
|
+
- shared: true when multiple connectors use the same provider
|
|
101
|
+
- auth_status: "ok" | "missing" | "invalid"
|
|
102
|
+
- auth_detail: aggregate auth summary or error message; null if missing
|
|
103
|
+
- auth_verified: true when credentials were live-checked with provider APIs
|
|
104
|
+
- accounts: authenticated account rows with account_id, label, workspace_id,
|
|
105
|
+
email, auth_method, auth_status, and auth_detail
|
|
106
|
+
"""
|
|
107
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
108
|
+
from agentgraph.connectors.status import auth_provider_status_items
|
|
109
|
+
|
|
110
|
+
bootstrap()
|
|
111
|
+
result = await auth_provider_status_items(get_all_connectors(), verify=verify)
|
|
112
|
+
return json.dumps(result)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
async def remove_auth_provider_tool(provider: str, account_id: str | None = None) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Remove stored credentials for an authentication provider.
|
|
119
|
+
|
|
120
|
+
This is the MCP equivalent of:
|
|
121
|
+
agentgraph auth remove <provider> [--account <account-id>]
|
|
122
|
+
|
|
123
|
+
Removing credentials stops authenticated connector operations such as
|
|
124
|
+
background polling, but does not delete already indexed graph data.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
provider: Auth provider key such as "google", "slack", or "discord".
|
|
128
|
+
account_id: Optional account ID. When omitted, all credentials for the
|
|
129
|
+
provider are removed.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
JSON object with provider, removed, and account_id when supplied.
|
|
133
|
+
"""
|
|
134
|
+
from agentgraph.auth.credentials import remove_platform, remove_platform_account
|
|
135
|
+
|
|
136
|
+
removed = (
|
|
137
|
+
remove_platform_account(provider, account_id)
|
|
138
|
+
if account_id is not None
|
|
139
|
+
else remove_platform(provider)
|
|
140
|
+
)
|
|
141
|
+
result: dict[str, object] = {
|
|
142
|
+
"provider": provider,
|
|
143
|
+
"removed": removed,
|
|
144
|
+
}
|
|
145
|
+
if account_id is not None:
|
|
146
|
+
result["account_id"] = account_id
|
|
147
|
+
return json.dumps(result)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@mcp.tool()
|
|
151
|
+
async def authenticate_provider_tool(
|
|
152
|
+
provider: str,
|
|
153
|
+
args: list[str] | None = None,
|
|
154
|
+
account_id: str | None = None,
|
|
155
|
+
add: bool = False,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Authenticate a credential-backed provider through its connector-owned flow.
|
|
158
|
+
|
|
159
|
+
This is the MCP equivalent of:
|
|
160
|
+
agentgraph auth <provider> [--account <account-id>] [--add] [provider options]
|
|
161
|
+
|
|
162
|
+
Slack accepts ``--method``, ``--client-id``, ``--xoxc-token``, and
|
|
163
|
+
``--d-cookie`` in ``args``. A Client ID implies OAuth; use the CLI for guided
|
|
164
|
+
app setup.
|
|
165
|
+
"""
|
|
166
|
+
import contextlib
|
|
167
|
+
import io
|
|
168
|
+
|
|
169
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
170
|
+
from agentgraph.connectors.status import run_auth_provider_flow
|
|
171
|
+
|
|
172
|
+
def _run() -> str:
|
|
173
|
+
output = io.StringIO()
|
|
174
|
+
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
|
|
175
|
+
run_auth_provider_flow(
|
|
176
|
+
get_all_connectors(),
|
|
177
|
+
provider,
|
|
178
|
+
account_id=account_id,
|
|
179
|
+
add=add,
|
|
180
|
+
args=args,
|
|
181
|
+
)
|
|
182
|
+
return output.getvalue()
|
|
183
|
+
|
|
184
|
+
bootstrap()
|
|
185
|
+
try:
|
|
186
|
+
output = await asyncio.to_thread(_run)
|
|
187
|
+
except ValueError as exc:
|
|
188
|
+
return json.dumps({"provider": provider, "authenticated": False, "error": str(exc)})
|
|
189
|
+
result: dict[str, object] = {"provider": provider, "authenticated": True}
|
|
190
|
+
if output:
|
|
191
|
+
result["output"] = output
|
|
192
|
+
return json.dumps(result)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
async def run_connector_command_tool(source: str, args: list[str]) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Run a connector-owned command.
|
|
199
|
+
|
|
200
|
+
This is the MCP equivalent of:
|
|
201
|
+
agentgraph connector <source> <args...>
|
|
202
|
+
|
|
203
|
+
Core dispatches to the connector generically; the connector owns command
|
|
204
|
+
names, argument parsing, and behaviour.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
source: Connector source, e.g. "rss".
|
|
208
|
+
args: Connector command and arguments, e.g.
|
|
209
|
+
["add", "https://simonwillison.net/atom/everything/"].
|
|
210
|
+
RSS add validates the supplied URLs as feeds before saving and
|
|
211
|
+
queues an RSS poll after a successful change.
|
|
212
|
+
RSS remove is available as:
|
|
213
|
+
["remove", "https://simonwillison.net/atom/everything/"].
|
|
214
|
+
RSS OPML import is also available as:
|
|
215
|
+
["import-opml", "/path/to/feeds.opml", "--all"] or
|
|
216
|
+
["import-opml", "/path/to/feeds.opml", "--select", "1,3-5"].
|
|
217
|
+
Connector-owned help is available as ["--help"].
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
JSON object returned by the connector, or an error.
|
|
221
|
+
"""
|
|
222
|
+
from agentgraph.connectors.registry import bootstrap, get_connector
|
|
223
|
+
|
|
224
|
+
bootstrap()
|
|
225
|
+
connector = get_connector(source)
|
|
226
|
+
if connector is None:
|
|
227
|
+
return json.dumps({"error": f"Unknown connector {source!r}"})
|
|
228
|
+
if args in (["--help"], ["help"]):
|
|
229
|
+
return json.dumps({"source": source, "help": type(connector).cli_help()})
|
|
230
|
+
try:
|
|
231
|
+
result = type(connector).run_cli_command(args)
|
|
232
|
+
effects = type(connector).command_effects(args, result)
|
|
233
|
+
if effects.poll:
|
|
234
|
+
from agentgraph.server.sync import schedule_poll_connector
|
|
235
|
+
|
|
236
|
+
result["poll"] = await schedule_poll_connector(connector)
|
|
237
|
+
return json.dumps(result, default=str)
|
|
238
|
+
except (NotImplementedError, OSError, ValueError) as exc:
|
|
239
|
+
return json.dumps({"error": str(exc)})
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool()
|
|
243
|
+
async def install_skill_tool(
|
|
244
|
+
skill: str = "graph", target: str = "user", force: bool = False
|
|
245
|
+
) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Install a bundled AgentGraph skill.
|
|
248
|
+
|
|
249
|
+
This is the MCP equivalent of:
|
|
250
|
+
agentgraph install-skill <skill> --target <user|project> [--force]
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
skill: Bundled skill name. Defaults to "graph".
|
|
254
|
+
target: "user" installs to ~/.agents/skills. "project" installs to
|
|
255
|
+
./.agents/skills relative to the MCP server process.
|
|
256
|
+
force: Overwrite an existing installed skill.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
JSON object with skill, target, source, destination, and overwritten,
|
|
260
|
+
or an error.
|
|
261
|
+
"""
|
|
262
|
+
from agentgraph.skills import SkillInstallError, install_skill
|
|
263
|
+
|
|
264
|
+
if target not in ("user", "project"):
|
|
265
|
+
return json.dumps({"error": "Target must be 'user' or 'project'"})
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = install_skill(skill, target=target, force=force)
|
|
269
|
+
return json.dumps(result.to_dict())
|
|
270
|
+
except SkillInstallError as exc:
|
|
271
|
+
return json.dumps({"error": str(exc)})
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def _enrich_results(results: list[dict[str, Any]]) -> None:
|
|
275
|
+
"""Let each owning connector apply result presentation fixes in-place."""
|
|
276
|
+
from agentgraph.connectors.registry import bootstrap, get_connector
|
|
277
|
+
|
|
278
|
+
bootstrap()
|
|
279
|
+
by_platform: dict[str, list[dict[str, Any]]] = {}
|
|
280
|
+
for entity in results:
|
|
281
|
+
platform = entity.get("platform")
|
|
282
|
+
if isinstance(platform, str):
|
|
283
|
+
by_platform.setdefault(platform, []).append(entity)
|
|
284
|
+
|
|
285
|
+
async def _enrich_one(platform: str, entities: list[dict[str, Any]]) -> None:
|
|
286
|
+
connector = get_connector(platform)
|
|
287
|
+
if connector is None:
|
|
288
|
+
return
|
|
289
|
+
try:
|
|
290
|
+
with timed("mcp.enrich_results", platform=platform, count=len(entities)):
|
|
291
|
+
await connector.enrich_results(entities)
|
|
292
|
+
except Exception:
|
|
293
|
+
logger.exception("Connector %s failed to enrich MCP results", platform)
|
|
294
|
+
|
|
295
|
+
if by_platform:
|
|
296
|
+
await asyncio.gather(
|
|
297
|
+
*(_enrich_one(platform, entities) for platform, entities in by_platform.items())
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# search_entities — hybrid vector + full-text, RRF fused
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@mcp.tool()
|
|
307
|
+
async def search_entities_tool(
|
|
308
|
+
query: str,
|
|
309
|
+
entity_types: list[str] | None = None,
|
|
310
|
+
platform: str | None = None,
|
|
311
|
+
limit: int = 10,
|
|
312
|
+
min_score: float = 0.03,
|
|
313
|
+
refresh: bool = False,
|
|
314
|
+
) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Search the knowledge graph using a natural-language query.
|
|
317
|
+
|
|
318
|
+
Combines semantic vector similarity and full-text search via
|
|
319
|
+
Reciprocal Rank Fusion for high-quality results.
|
|
320
|
+
|
|
321
|
+
IMPORTANT — attachments: chat photos, images, and uploaded files are
|
|
322
|
+
stored as attachments on Message entities (in metadata.attachments).
|
|
323
|
+
Gmail email attachments are represented as Gmail Document stubs referenced
|
|
324
|
+
by the owning Thread and can be downloaded with download_entity_tool.
|
|
325
|
+
If the user asks about chat uploads, search Message entities or use
|
|
326
|
+
query_by_filter_tool with has_attachments=True. If the user asks about
|
|
327
|
+
Gmail attachments, inspect the Thread's referenced Document stubs.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
query: Natural-language search query.
|
|
331
|
+
entity_types: Optional list of entity types to restrict results
|
|
332
|
+
(e.g. ["Message", "Document", "Channel"]). To find chat images or
|
|
333
|
+
attachments, pass ["Message"]. To find Gmail attachment stubs, pass
|
|
334
|
+
["Document"] and platform="gmail".
|
|
335
|
+
platform: Optional platform name to scope the search to a single
|
|
336
|
+
source (e.g. "slack", "discord", "gdocs", "gmail", "rss"). When
|
|
337
|
+
omitted, all platforms are searched. Use this to avoid
|
|
338
|
+
cross-platform noise when the user specifies a source.
|
|
339
|
+
limit: Maximum number of results to return (default 10).
|
|
340
|
+
min_score: Minimum relevance score threshold (0–1, default 0.02).
|
|
341
|
+
Results below this score are suppressed as noise.
|
|
342
|
+
refresh: If true, let connectors refresh or enrich connector-owned
|
|
343
|
+
presentation metadata before returning. Defaults to false to keep
|
|
344
|
+
search responsive.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
JSON array of matching entities with id, title, content snippet,
|
|
348
|
+
platform, and relevance score. Connectors may refresh or enrich
|
|
349
|
+
connector-owned metadata before results are returned.
|
|
350
|
+
"""
|
|
351
|
+
results = await search_entities(
|
|
352
|
+
query, entity_types=entity_types, limit=limit, min_score=min_score, platform=platform
|
|
353
|
+
)
|
|
354
|
+
for r in results:
|
|
355
|
+
_truncate_content(r)
|
|
356
|
+
if refresh:
|
|
357
|
+
await _enrich_results(results)
|
|
358
|
+
return json.dumps(results, default=str)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# get_entity — full entity by UUID
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
async def get_entity_tool(entity_id: str) -> str:
|
|
368
|
+
"""
|
|
369
|
+
Retrieve full details for a single existing entity.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
entity_id: Entity UUID, UUID prefix, platform ref, or HTTP(S) URL.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
JSON object with all entity fields, or an error message if not found.
|
|
376
|
+
"""
|
|
377
|
+
from agentgraph.graph.query import get_entity_by_url, is_http_url
|
|
378
|
+
|
|
379
|
+
entity = (
|
|
380
|
+
await get_entity_by_url(entity_id)
|
|
381
|
+
if is_http_url(entity_id)
|
|
382
|
+
else await get_entity(entity_id)
|
|
383
|
+
)
|
|
384
|
+
if entity is None:
|
|
385
|
+
return json.dumps({"error": f"Entity {entity_id!r} not found"})
|
|
386
|
+
return json.dumps(entity, default=str)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# get_edges — edges connected to an entity
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@mcp.tool()
|
|
395
|
+
async def get_edges_tool(
|
|
396
|
+
entity_id: str,
|
|
397
|
+
edge_type: str | None = None,
|
|
398
|
+
direction: str = "both",
|
|
399
|
+
) -> str:
|
|
400
|
+
"""
|
|
401
|
+
List edges connected to an entity.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
entity_id: UUID of the entity.
|
|
405
|
+
edge_type: Optional edge type filter (e.g. "authored", "posted_in",
|
|
406
|
+
"replied_to", "mentions", "collaborated").
|
|
407
|
+
direction: "in" (incoming), "out" (outgoing), or "both" (default).
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
JSON array of edge objects including source/target references.
|
|
411
|
+
"""
|
|
412
|
+
edges = await get_edges(entity_id, edge_type=edge_type, direction=direction)
|
|
413
|
+
return json.dumps(edges, default=str)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# traverse_graph — BFS neighbourhood
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@mcp.tool()
|
|
422
|
+
async def traverse_graph_tool(
|
|
423
|
+
entity_id: str,
|
|
424
|
+
max_depth: int = 2,
|
|
425
|
+
) -> str:
|
|
426
|
+
"""
|
|
427
|
+
Traverse the knowledge graph from a starting entity using BFS.
|
|
428
|
+
|
|
429
|
+
Useful for discovering the context around a message or document:
|
|
430
|
+
who authored it, which channel it appeared in, what it references, etc.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
entity_id: UUID of the starting entity.
|
|
434
|
+
max_depth: Maximum number of hops to traverse (default 2, max 4).
|
|
435
|
+
A depth of 0 returns only the starting entity.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
JSON object with "nodes" (entities) and "edges" lists.
|
|
439
|
+
"""
|
|
440
|
+
depth = min(max(max_depth, 0), 4)
|
|
441
|
+
result = await traverse_graph(entity_id, max_depth=depth)
|
|
442
|
+
# Trim content on nodes to keep response size manageable
|
|
443
|
+
for node in result.get("nodes", []):
|
|
444
|
+
if node.get("content") and len(str(node["content"])) > 300:
|
|
445
|
+
node["content"] = str(node["content"])[:300] + "…"
|
|
446
|
+
return json.dumps(result, default=str)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
# fetch_entity — trigger connector re-ingestion
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@mcp.tool()
|
|
455
|
+
async def fetch_entity_tool(platform: str, resource_id: str) -> str:
|
|
456
|
+
"""
|
|
457
|
+
Trigger a connector fetch for a platform entity.
|
|
458
|
+
|
|
459
|
+
Forces re-ingestion of a specific resource from its source platform,
|
|
460
|
+
persisting updated content, people, and edges before returning.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
platform: Platform name (e.g. "gdocs", "slack", "discord", "gmail", "rss").
|
|
464
|
+
resource_id: Platform-specific entity ID.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
JSON object with counts of ingested entities, persons, and edges.
|
|
468
|
+
"""
|
|
469
|
+
from agentgraph.graph.fetch import fetch_entity
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
result = await fetch_entity(platform, resource_id)
|
|
473
|
+
return json.dumps(result)
|
|
474
|
+
except ValueError as exc:
|
|
475
|
+
return json.dumps({"error": str(exc)})
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
# fetch_entity_by_id — re-ingest by internal UUID
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@mcp.tool()
|
|
484
|
+
async def fetch_entity_by_id_tool(entity_id: str) -> str:
|
|
485
|
+
"""
|
|
486
|
+
Trigger a connector fetch for an entity by its internal UUID.
|
|
487
|
+
|
|
488
|
+
Looks up the entity's platform and platform-specific ID, then forces
|
|
489
|
+
re-ingestion from the source platform and persists the returned batch
|
|
490
|
+
before returning.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
entity_id: Internal entity UUID (the id field from graph nodes).
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
JSON object with counts of ingested entities, persons, and edges,
|
|
497
|
+
or an error message if the entity is not found.
|
|
498
|
+
"""
|
|
499
|
+
from agentgraph.graph.fetch import fetch_entity_by_id
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
result = await fetch_entity_by_id(entity_id)
|
|
503
|
+
return json.dumps(result)
|
|
504
|
+
except ValueError as exc:
|
|
505
|
+
return json.dumps({"error": str(exc)})
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
# poll_connectors — trigger background connector polling
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@mcp.tool()
|
|
514
|
+
async def poll_connectors_tool(source: str | None = None) -> str:
|
|
515
|
+
"""
|
|
516
|
+
Trigger a background poll for one connector or all polling connectors.
|
|
517
|
+
|
|
518
|
+
This is the MCP equivalent of:
|
|
519
|
+
agentgraph poll [<source>]
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
source: Optional connector source to poll. When omitted, all connectors
|
|
523
|
+
with poll_interval configured are polled.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
JSON object with queued, already-running, and skipped connector sources,
|
|
527
|
+
or an error if a requested connector source is not registered.
|
|
528
|
+
"""
|
|
529
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors, get_connector
|
|
530
|
+
from agentgraph.server.sync import schedule_poll_connector
|
|
531
|
+
|
|
532
|
+
bootstrap()
|
|
533
|
+
if source is not None:
|
|
534
|
+
connector = get_connector(source)
|
|
535
|
+
if connector is None:
|
|
536
|
+
return json.dumps({"error": f"No connector registered for source {source!r}"})
|
|
537
|
+
connectors = [connector]
|
|
538
|
+
else:
|
|
539
|
+
connectors = get_all_connectors()
|
|
540
|
+
|
|
541
|
+
polled: list[str] = []
|
|
542
|
+
already_running: list[str] = []
|
|
543
|
+
skipped: list[dict[str, str | None]] = []
|
|
544
|
+
for connector in connectors:
|
|
545
|
+
if connector.poll_interval is None:
|
|
546
|
+
continue
|
|
547
|
+
result = await schedule_poll_connector(connector)
|
|
548
|
+
if result["status"] == "queued":
|
|
549
|
+
polled.append(connector.source)
|
|
550
|
+
elif result["status"] == "already_running":
|
|
551
|
+
already_running.append(connector.source)
|
|
552
|
+
else:
|
|
553
|
+
skipped.append({"source": connector.source, "reason": result["reason"]})
|
|
554
|
+
|
|
555
|
+
return json.dumps({"polled": polled, "already_running": already_running, "skipped": skipped})
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
# ingest_connector — trigger background bulk ingest
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@mcp.tool()
|
|
564
|
+
async def ingest_connector_tool(source: str) -> str:
|
|
565
|
+
"""
|
|
566
|
+
Trigger a background one-shot bulk ingest for a connector.
|
|
567
|
+
|
|
568
|
+
This is the MCP equivalent of:
|
|
569
|
+
agentgraph ingest <source>
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
source: Connector source to ingest.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
JSON object with source and status, or an error if the connector source
|
|
576
|
+
is not registered.
|
|
577
|
+
"""
|
|
578
|
+
from agentgraph.connectors.registry import bootstrap, get_connector
|
|
579
|
+
from agentgraph.server.sync import run_ingest
|
|
580
|
+
|
|
581
|
+
bootstrap()
|
|
582
|
+
connector = get_connector(source)
|
|
583
|
+
if connector is None:
|
|
584
|
+
return json.dumps({"error": f"No connector registered for source {source!r}"})
|
|
585
|
+
|
|
586
|
+
asyncio.create_task(run_ingest(connector))
|
|
587
|
+
return json.dumps({"source": source, "status": "started"})
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ---------------------------------------------------------------------------
|
|
591
|
+
# download_entity — authenticated source-file download
|
|
592
|
+
# ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@mcp.tool()
|
|
596
|
+
async def download_entity_tool(entity_id: str, output_path: str | None = None) -> str:
|
|
597
|
+
"""
|
|
598
|
+
Download an entity's source file using the connector's stored auth.
|
|
599
|
+
|
|
600
|
+
Supports entity UUIDs, UUID prefixes, and platform refs such as
|
|
601
|
+
"gdrive/file-id" or "gmail/document/attachment/<message-id>/<attachment-id>"
|
|
602
|
+
when those resolve to a graph entity. The file is written to output_path
|
|
603
|
+
when supplied, or to the MCP server's current directory using the source
|
|
604
|
+
filename.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
entity_id: Entity UUID, UUID prefix, or platform/entity_id reference.
|
|
608
|
+
output_path: Optional output file path or directory.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
JSON object with path, byte count, filename, platform, and MIME type, or
|
|
612
|
+
an error message if the entity or connector cannot be downloaded.
|
|
613
|
+
"""
|
|
614
|
+
from agentgraph.graph.download import download_entity
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
result = await download_entity(entity_id, output_path)
|
|
618
|
+
return json.dumps(result, default=str)
|
|
619
|
+
except ValueError as exc:
|
|
620
|
+
return json.dumps({"error": str(exc)})
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
# bookmark_entity — protect an entity from garbage collection
|
|
625
|
+
# ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@mcp.tool()
|
|
629
|
+
async def bookmark_entity_tool(entity_id: str, bookmarked: bool = True) -> str:
|
|
630
|
+
"""
|
|
631
|
+
Set or remove bookmark protection for an entity or URL.
|
|
632
|
+
|
|
633
|
+
Supports entity UUIDs, UUID prefixes, and platform refs such as
|
|
634
|
+
"gdrive/file-id" when those resolve to a graph entity. HTTP(S) URLs are
|
|
635
|
+
fetched through an owning connector when possible when adding a bookmark,
|
|
636
|
+
otherwise through the generic web connector.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
entity_id: Entity UUID, UUID prefix, platform/entity_id reference, or URL.
|
|
640
|
+
bookmarked: True to add bookmark protection; false to remove it.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
JSON object for the updated entity with its bookmark state, or an error
|
|
644
|
+
message if the entity cannot be found.
|
|
645
|
+
"""
|
|
646
|
+
from agentgraph.graph.bookmark import bookmark_target, set_entity_bookmark
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
result = (
|
|
650
|
+
await bookmark_target(entity_id)
|
|
651
|
+
if bookmarked
|
|
652
|
+
else await set_entity_bookmark(entity_id, False)
|
|
653
|
+
)
|
|
654
|
+
return json.dumps(result, default=str)
|
|
655
|
+
except ValueError as exc:
|
|
656
|
+
return json.dumps({"error": str(exc)})
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# ---------------------------------------------------------------------------
|
|
660
|
+
# delete_entity — remove an entity
|
|
661
|
+
# ---------------------------------------------------------------------------
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@mcp.tool()
|
|
665
|
+
async def delete_entity_tool(entity_id: str) -> str:
|
|
666
|
+
"""
|
|
667
|
+
Delete an entity from the graph.
|
|
668
|
+
|
|
669
|
+
Supports entity UUIDs, UUID prefixes, platform refs such as
|
|
670
|
+
"gdrive/file-id", and HTTP(S) URLs when those resolve to a graph entity.
|
|
671
|
+
Connected edges are deleted with the entity.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
entity_id: Entity UUID, UUID prefix, platform/entity_id reference, or URL.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
JSON object with deleted=true and the deleted entity, or an error
|
|
678
|
+
message if the entity cannot be found.
|
|
679
|
+
"""
|
|
680
|
+
from agentgraph.graph.delete import delete_entity
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
result = await delete_entity(entity_id)
|
|
684
|
+
return json.dumps(result, default=str)
|
|
685
|
+
except ValueError as exc:
|
|
686
|
+
return json.dumps({"error": str(exc)})
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# ---------------------------------------------------------------------------
|
|
690
|
+
# unify_persons — manually merge duplicate Person entities
|
|
691
|
+
# ---------------------------------------------------------------------------
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@mcp.tool()
|
|
695
|
+
async def unify_persons_tool(
|
|
696
|
+
primary_entity_id: str,
|
|
697
|
+
duplicate_entity_ids: list[str],
|
|
698
|
+
) -> str:
|
|
699
|
+
"""
|
|
700
|
+
Merge duplicate Person entities that refer to the same human.
|
|
701
|
+
|
|
702
|
+
Use this only when the user has confirmed that the Person entities are the
|
|
703
|
+
same person. The primary Person keeps its ID; edges from duplicate Persons
|
|
704
|
+
are rewired to the primary; duplicate metadata such as platform user IDs is
|
|
705
|
+
folded into the primary; duplicate Person entities are removed.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
primary_entity_id: Person entity ID, UUID prefix, or platform ref to keep.
|
|
709
|
+
duplicate_entity_ids: Duplicate Person entity IDs, UUID prefixes, or
|
|
710
|
+
platform refs to merge into the primary.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
JSON object with the updated primary Person and merged duplicate IDs,
|
|
714
|
+
or an error message if any entity is missing or is not a Person.
|
|
715
|
+
"""
|
|
716
|
+
from agentgraph.graph.person import unify_persons
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
result = await unify_persons(primary_entity_id, duplicate_entity_ids)
|
|
720
|
+
return json.dumps(result, default=str)
|
|
721
|
+
except ValueError as exc:
|
|
722
|
+
return json.dumps({"error": str(exc)})
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# query_by_filter — type + metadata filters
|
|
727
|
+
# ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@mcp.tool()
|
|
731
|
+
async def query_by_filter_tool(
|
|
732
|
+
entity_type: str,
|
|
733
|
+
filters: dict[str, Any] | None = None,
|
|
734
|
+
since: str | None = None,
|
|
735
|
+
authored_by_me: bool = False,
|
|
736
|
+
has_attachments: bool = False,
|
|
737
|
+
limit: int = 50,
|
|
738
|
+
order_by: str = "created_at",
|
|
739
|
+
refresh: bool = False,
|
|
740
|
+
) -> str:
|
|
741
|
+
"""
|
|
742
|
+
Query entities by type with optional filters.
|
|
743
|
+
|
|
744
|
+
Useful for listing all messages in a specific channel, all documents
|
|
745
|
+
on a platform, activity within a time window, or content authored by
|
|
746
|
+
the current user.
|
|
747
|
+
|
|
748
|
+
Entity types and what they contain:
|
|
749
|
+
- Message: chat messages from Discord, Slack, etc. This is
|
|
750
|
+
where chat image and file uploads live — attachments are stored
|
|
751
|
+
in metadata.attachments as a JSON array with fields: url,
|
|
752
|
+
filename, content_type, width, height. To find images or
|
|
753
|
+
uploaded files, query Message (not Document) and set
|
|
754
|
+
has_attachments=True.
|
|
755
|
+
- Document: text documents such as Google Docs, plus Gmail attachment
|
|
756
|
+
stubs referenced by their owning Thread. Gmail attachment Document
|
|
757
|
+
stubs can be passed to download_entity_tool.
|
|
758
|
+
- Spreadsheet: Google Sheets or Excel files.
|
|
759
|
+
- Folder: a Google Drive folder containing other entities.
|
|
760
|
+
- Channel: a chat channel or DM thread (Discord, Slack, etc.).
|
|
761
|
+
- Thread: an email thread (Gmail).
|
|
762
|
+
- Task: a task or to-do item (e.g. from a project tracker).
|
|
763
|
+
- Project: a project or repository container.
|
|
764
|
+
|
|
765
|
+
Example — find images uploaded in the last 7 days:
|
|
766
|
+
entity_type="Message", has_attachments=True, since="7d"
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
entity_type: Entity type to query. See above for what each type
|
|
770
|
+
contains. Use "Message" to find chat uploads; use "Document" with
|
|
771
|
+
platform="gmail" to find Gmail attachment stubs.
|
|
772
|
+
filters: Optional dict of key=value filters. Known columns
|
|
773
|
+
(platform, platform_entity_id) are applied as column filters;
|
|
774
|
+
all other keys are matched against the metadata JSONB field.
|
|
775
|
+
since: Optional time cutoff — ISO timestamp or relative duration
|
|
776
|
+
like "12h", "30m", "2d". Only returns entities updated after
|
|
777
|
+
this time.
|
|
778
|
+
authored_by_me: If true, only return entities with an authored
|
|
779
|
+
edge from the current user (resolved from stored credentials).
|
|
780
|
+
has_attachments: If true, only return Message entities that have
|
|
781
|
+
at least one chat file or image attachment in metadata.attachments.
|
|
782
|
+
Ignored for non-Message entity types. Gmail attachments are
|
|
783
|
+
Document stubs instead.
|
|
784
|
+
limit: Maximum number of results (default 50).
|
|
785
|
+
order_by: Column to sort by descending (default "created_at").
|
|
786
|
+
refresh: If true, let connectors refresh or enrich connector-owned
|
|
787
|
+
presentation metadata before returning. Defaults to false to keep
|
|
788
|
+
queries responsive.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
JSON array of matching entities. For Message entities with
|
|
792
|
+
attachments, each result includes metadata.attachments — a JSON
|
|
793
|
+
string that decodes to a list of {url, filename, content_type,
|
|
794
|
+
width?, height?} objects. Connectors may refresh or enrich
|
|
795
|
+
connector-owned metadata before results are returned.
|
|
796
|
+
"""
|
|
797
|
+
str_filters: dict[str, str] = {k: str(v) for k, v in (filters or {}).items()}
|
|
798
|
+
results = await query_by_filter(
|
|
799
|
+
entity_type,
|
|
800
|
+
filters=str_filters,
|
|
801
|
+
limit=limit,
|
|
802
|
+
order_by=order_by,
|
|
803
|
+
since=since,
|
|
804
|
+
authored_by_me=authored_by_me,
|
|
805
|
+
has_attachments=has_attachments,
|
|
806
|
+
)
|
|
807
|
+
for result in results:
|
|
808
|
+
_truncate_content(result)
|
|
809
|
+
if refresh:
|
|
810
|
+
await _enrich_results(results)
|
|
811
|
+
return json.dumps(results, default=str)
|