@yottagraph-app/aether-instructions 1.1.33 → 1.1.34

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottagraph-app/aether-instructions",
3
- "version": "1.1.33",
3
+ "version": "1.1.34",
4
4
  "description": "Cursor rules, commands, and skills for Aether development",
5
5
  "files": [
6
6
  "rules",
@@ -80,10 +80,69 @@ etc.) handle entity resolution, NEID formatting, and schema lookups
80
80
  automatically. Use the `data-model` skill docs for entity types,
81
81
  properties, and relationship schemas.
82
82
 
83
- Wire MCP into ADK agents by declaring an `McpToolset` that points to the
84
- Elemental MCP server. The MCP server URL and auth are configured in the
85
- agent's runtime environment (typically via `broadchurch.yaml` gateway
86
- proxy). See the Aether `agents` rule for ADK agent structure.
83
+ ### Wiring McpToolset (transport class)
84
+
85
+ The Elemental MCP server uses **Streamable HTTP** transport. Use
86
+ `StreamableHTTPConnectionParams` **NOT** `SseConnectionParams`. The
87
+ `SseConnectionParams` class is for older SSE-based MCP servers and will
88
+ silently fail against the Elemental server (the agent starts with zero
89
+ tools and the LLM hallucinates code).
90
+
91
+ ```python
92
+ from google.adk.tools.mcp_tool import McpToolset
93
+ from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
94
+
95
+ McpToolset(
96
+ connection_params=StreamableHTTPConnectionParams(url=mcp_url)
97
+ )
98
+ ```
99
+
100
+ Resolve the MCP URL from the environment or `broadchurch.yaml`:
101
+
102
+ ```python
103
+ import os
104
+ from pathlib import Path
105
+ import yaml
106
+
107
+ def _get_mcp_url(server_name: str = "elemental") -> str:
108
+ """Resolve MCP server URL from env or broadchurch.yaml."""
109
+ env_url = os.environ.get("ELEMENTAL_MCP_URL")
110
+ if env_url:
111
+ return env_url
112
+ for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
113
+ if candidate.exists():
114
+ config = yaml.safe_load(candidate.read_text()) or {}
115
+ gw = config.get("gateway", {})
116
+ org_id = config.get("tenant", {}).get("org_id", "")
117
+ if gw.get("url") and org_id:
118
+ return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
119
+ return config.get("mcp", {}).get(server_name, "")
120
+ return ""
121
+ ```
122
+
123
+ For **local dev**, set the env var:
124
+ ```bash
125
+ export ELEMENTAL_MCP_URL="https://mcp.news.prod.g.lovelace.ai/elemental/mcp"
126
+ ```
127
+
128
+ In **production**, the agent reads the gateway URL and org_id from
129
+ `broadchurch.yaml` and routes through the Portal MCP proxy, which handles
130
+ authentication automatically.
131
+
132
+ ### Silent failure warning
133
+
134
+ If `McpToolset` cannot connect (wrong transport class, bad URL, or server
135
+ down), **the agent starts with zero MCP tools and no error is raised**.
136
+ The LLM will then hallucinate tool calls instead of executing real ones.
137
+ Always validate the MCP URL at agent startup:
138
+
139
+ ```python
140
+ mcp_url = _get_mcp_url()
141
+ if not mcp_url:
142
+ raise RuntimeError("No MCP URL — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
143
+ ```
144
+
145
+ ### MCP response patterns
87
146
 
88
147
  **Read the `elemental-mcp-patterns` skill** (`skills/elemental-mcp-patterns/`)
89
148
  before writing tool code. It covers MCP response shapes, property type
package/rules/agents.mdc CHANGED
@@ -48,7 +48,11 @@ Key rules:
48
48
  - Pin dependency versions in `requirements.txt` for reproducible deployments
49
49
 
50
50
  For agents that call the **Elemental API** (`broadchurch_auth`, endpoints,
51
- local `ELEMENTAL_*` env vars), see the `agents-data` rule.
51
+ local `ELEMENTAL_*` env vars), see the `agents-data` rule. For agents
52
+ using **Elemental MCP tools**, the `agents-data` rule also covers
53
+ `McpToolset` wiring — use `StreamableHTTPConnectionParams` (not
54
+ `SseConnectionParams`) and read the `elemental-mcp-patterns` skill for
55
+ response handling patterns.
52
56
 
53
57
  ## Local Testing
54
58
 
@@ -0,0 +1,621 @@
1
+ ---
2
+ name: elemental-mcp-patterns
3
+ description: How to correctly call Elemental MCP tools and process their responses when writing Python ADK agent tool functions.
4
+ ---
5
+
6
+ # Elemental MCP Patterns
7
+
8
+ This skill is for **build agents writing Python tool code** that calls
9
+ Elemental MCP tools. It covers MCP wiring, transport configuration,
10
+ response shapes, property type handling, and copy-paste patterns for
11
+ common operations.
12
+
13
+ For domain knowledge (what entity types and properties exist), see the
14
+ **data-model** skill.
15
+
16
+ ---
17
+
18
+ ## MCP Wiring: Connecting ADK Agents to Elemental MCP
19
+
20
+ ### Transport class
21
+
22
+ The Elemental MCP server uses **Streamable HTTP** transport. You **must**
23
+ use `StreamableHTTPConnectionParams`:
24
+
25
+ ```python
26
+ from google.adk.tools.mcp_tool import McpToolset
27
+ from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
28
+ ```
29
+
30
+ > **Do NOT use `SseConnectionParams`.** `SseConnectionParams` is for
31
+ > legacy SSE-based MCP servers. Using it against the Elemental MCP server
32
+ > will silently fail — the agent starts with zero tools and no error is
33
+ > raised. The LLM then hallucinates tool calls.
34
+
35
+ ### Resolving the MCP URL
36
+
37
+ The MCP URL comes from the environment (`ELEMENTAL_MCP_URL`) for local
38
+ dev, or from `broadchurch.yaml` for deployed agents:
39
+
40
+ ```python
41
+ import os
42
+ from pathlib import Path
43
+ import yaml
44
+
45
+ def _get_mcp_url(server_name: str = "elemental") -> str:
46
+ """Resolve MCP server URL from env or broadchurch.yaml."""
47
+ env_url = os.environ.get("ELEMENTAL_MCP_URL")
48
+ if env_url:
49
+ return env_url
50
+ for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
51
+ if candidate.exists():
52
+ config = yaml.safe_load(candidate.read_text()) or {}
53
+ gw = config.get("gateway", {})
54
+ org_id = config.get("tenant", {}).get("org_id", "")
55
+ if gw.get("url") and org_id:
56
+ return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
57
+ return config.get("mcp", {}).get(server_name, "")
58
+ return ""
59
+ ```
60
+
61
+ ### Complete wiring example
62
+
63
+ ```python
64
+ from google.adk.agents import Agent
65
+ from google.adk.tools.mcp_tool import McpToolset
66
+ from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
67
+
68
+ mcp_url = _get_mcp_url() # function from above
69
+ if not mcp_url:
70
+ raise RuntimeError("No MCP URL — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
71
+
72
+ root_agent = Agent(
73
+ model="gemini-2.0-flash",
74
+ name="my_mcp_agent",
75
+ instruction="You are a research assistant with access to the Lovelace knowledge graph.",
76
+ tools=[
77
+ my_custom_tool, # your Python tool functions
78
+ McpToolset(connection_params=StreamableHTTPConnectionParams(url=mcp_url)),
79
+ ],
80
+ )
81
+ ```
82
+
83
+ The `McpToolset` automatically discovers all MCP tools at startup and
84
+ exposes them to the agent. To restrict which tools are exposed, use
85
+ `tool_filter`:
86
+
87
+ ```python
88
+ McpToolset(
89
+ connection_params=StreamableHTTPConnectionParams(url=mcp_url),
90
+ tool_filter=["elemental_get_entity", "elemental_get_related", "elemental_get_events"],
91
+ )
92
+ ```
93
+
94
+ ### Silent failure mode
95
+
96
+ If `McpToolset` cannot connect, **no error is raised at agent startup**.
97
+ The agent simply has zero MCP tools. Symptoms:
98
+ - The agent never calls any `elemental_*` tools
99
+ - The LLM fabricates code or data instead of using tools
100
+ - No connection error in logs
101
+
102
+ **Always validate** the MCP URL and check for tool availability during
103
+ development. If MCP tools aren't working, verify:
104
+ 1. The URL is correct (check `broadchurch.yaml` `mcp.elemental` or env var)
105
+ 2. You're using `StreamableHTTPConnectionParams` (not `SseConnectionParams`)
106
+ 3. The MCP server is reachable (try `curl -X POST <url> -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'`)
107
+
108
+ ### Local dev setup
109
+
110
+ ```bash
111
+ export ELEMENTAL_MCP_URL="https://mcp.news.prod.g.lovelace.ai/elemental/mcp"
112
+ export GOOGLE_CLOUD_PROJECT=broadchurch
113
+ export GOOGLE_CLOUD_LOCATION=us-central1
114
+ export GOOGLE_GENAI_USE_VERTEXAI=1
115
+ cd agents/
116
+ adk web
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Tool Quick Reference
122
+
123
+ | Tool | Use when you need to... |
124
+ |---|---|
125
+ | `elemental_get_entity` | Resolve an entity by name or ID, fetch its properties (supports `history` for time-series) |
126
+ | `elemental_get_related` | Find related entities (requires `related_flavor`); use `direction` and `relationship_types` to filter |
127
+ | `elemental_get_events` | Get typed events with categories, dates, participants |
128
+ | `elemental_get_citations` | Look up provenance for `ref` hashes returned by other tools |
129
+ | `elemental_get_schema` | Discover flavors (entity types), property names, and property types |
130
+ | `elemental_get_relationships` | Get relationship types and counts between two entities |
131
+ | `elemental_graph_neighborhood` | Get the most influential neighbors of an entity |
132
+ | `elemental_graph_sentiment` | Sentiment time series, trend analysis, and statistics from news articles |
133
+ | `elemental_introspect` | Discover what data **actually exists**: entity counts, populated properties with fill rates, sample values. Use before building features to verify data availability. |
134
+ | `elemental_traverse` | Stateful graph navigation — build a working set of entities across multiple calls (start → expand → filter → inspect) |
135
+ | `elemental_health` | Health check — verify MCP server connectivity |
136
+
137
+ ### MCP Prompts
138
+
139
+ The server also exposes built-in prompts that provide pre-composed
140
+ workflows:
141
+
142
+ | Prompt | Purpose |
143
+ |---|---|
144
+ | `company-deep-dive` | Comprehensive company research workflow |
145
+ | `blast-radius` | Analyze impact/connections radiating from an entity |
146
+ | `event-monitor` | Track events and developments for entities |
147
+
148
+ ### MCP Resources
149
+
150
+ Documentation resources are available directly from the server:
151
+
152
+ | Resource | Description |
153
+ |---|---|
154
+ | `elemental_data_model` | Entity types, properties, and relationships |
155
+ | `elemental_guide` | Usage guide for the MCP tools |
156
+ | `elemental_schema` | Live schema data |
157
+ | `elemental_workflows` | Common workflow patterns and examples |
158
+ | `elemental_mcp_server_info` | Server capabilities and configuration |
159
+
160
+ These prompts and resources can be useful shortcuts — check if your MCP
161
+ client supports them before building equivalent logic from scratch.
162
+
163
+ ---
164
+
165
+ ## Response Shapes
166
+
167
+ Every MCP tool call returns a JSON dict. Below are annotated examples of
168
+ the three tools you'll use most.
169
+
170
+ ### elemental_get_entity
171
+
172
+ ```python
173
+ result = await mcp_call("elemental_get_entity", {
174
+ "entity": "Intel",
175
+ "properties": ["country", "total_revenue", "ticker_symbol", "net_income"]
176
+ })
177
+
178
+ # Response shape:
179
+ {
180
+ "entity": {
181
+ "neid": "04926132345040704022",
182
+ "name": "Intel Corporation",
183
+ "flavor": "organization",
184
+ "properties": {
185
+ "country": {
186
+ "value": "5816460566439750832", # <-- THIS IS A NEID, NOT A NAME
187
+ "ref": "ref_a3f2b1c8"
188
+ },
189
+ "total_revenue": {
190
+ "value": 52900000000,
191
+ "ref": "ref_d4e5f678",
192
+ "attributes": {"filing_period": "FY"}
193
+ },
194
+ "ticker_symbol": {
195
+ "value": "INTC"
196
+ },
197
+ "net_income": {
198
+ "value": -267000000,
199
+ "ref": "ref_b2c3d4e5"
200
+ }
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ Key points:
207
+ - `entity` is `null` if resolution failed
208
+ - Each property value is `{"value": ..., "ref"?: "...", "attributes"?: {...}}`
209
+ - **`value` can be a NEID** for reference-typed properties — see
210
+ "The Property Type Problem" below
211
+ - `ref` is a citation hash — pass it through to the LLM exactly as-is
212
+ - `low_confidence` means the entity match is fuzzy — confirm with the user
213
+
214
+ ### elemental_get_related
215
+
216
+ ```python
217
+ result = await mcp_call("elemental_get_related", {
218
+ "entity": "Intel",
219
+ "related_flavor": "person", # REQUIRED
220
+ "relationship_types": ["board_member_of"], # optional filter
221
+ "related_properties": ["nationality", "title"], # properties on each person
222
+ "limit": 20
223
+ })
224
+
225
+ # Response shape:
226
+ {
227
+ "resolved": {
228
+ "neid": "04926132345040704022",
229
+ "name": "Intel Corporation",
230
+ "flavor": "organization"
231
+ },
232
+ "total": 12,
233
+ "relationships": [
234
+ {
235
+ "neid": "08371625409283746152",
236
+ "name": "Patrick Gelsinger",
237
+ "flavor": "person",
238
+ "relationship_types": ["board_member_of"],
239
+ "properties": {
240
+ "nationality": {"value": "United States"},
241
+ "title": {"value": "CEO"}
242
+ }
243
+ }
244
+ # ... more related entities
245
+ ]
246
+ }
247
+ ```
248
+
249
+ Key points:
250
+ - `related_flavor` is **required** — you must specify what type of entity
251
+ to look for
252
+ - `resolved` is the center entity (can be `null` if resolution failed)
253
+ - Each item in `relationships` has the same property value shape as
254
+ `elemental_get_entity`
255
+ - Use `direction` (`"outgoing"`, `"incoming"`, `"both"`) to control
256
+ traversal direction
257
+
258
+ ### elemental_get_events
259
+
260
+ ```python
261
+ result = await mcp_call("elemental_get_events", {
262
+ "entity": "Intel",
263
+ "categories": ["Bankruptcy", "IPO", "Regulatory Action"], # optional
264
+ "time_range": {"after": "2025-01-01"}, # optional
265
+ "include_participants": True, # optional
266
+ "limit": 20
267
+ })
268
+
269
+ # Response shape:
270
+ {
271
+ "resolved": {
272
+ "neid": "04926132345040704022",
273
+ "name": "Intel Corporation"
274
+ },
275
+ "total": 3,
276
+ "events": [
277
+ {
278
+ "neid": "09283746152837461528",
279
+ "name": "Intel CHIPS Act Award",
280
+ "flavor": "event",
281
+ "properties": {
282
+ "category": {"value": "Regulatory Action"},
283
+ "date": {"value": "2025-03-15"},
284
+ "description": {"value": "Intel awarded $8.5B in CHIPS Act funding"},
285
+ "likelihood": {"value": 0.95}
286
+ },
287
+ "participants": [
288
+ {
289
+ "neid": "04926132345040704022",
290
+ "name": "Intel Corporation",
291
+ "flavor": "organization",
292
+ "relationship_types": ["participant"]
293
+ }
294
+ ]
295
+ }
296
+ ]
297
+ }
298
+ ```
299
+
300
+ Key points:
301
+ - Events have typed fields: `category`, `date`, `description`, `likelihood`
302
+ - Use `categories` to filter — do NOT try to find events by scanning
303
+ property names for keywords
304
+ - `participants` only included when `include_participants` is `True`
305
+
306
+ ---
307
+
308
+ ## The Property Type Problem
309
+
310
+ This is the single biggest source of bugs in MCP-based agents.
311
+
312
+ **The issue:** property values can be entity references (NEIDs), not
313
+ display text. If you render them raw, the user sees `"5816460566439750832"`
314
+ instead of `"United States"`.
315
+
316
+ ### Step 1: Get the schema to learn property types
317
+
318
+ ```python
319
+ schema = await mcp_call("elemental_get_schema", {"flavor": "organization"})
320
+
321
+ # Response includes a properties array:
322
+ # [
323
+ # {"name": "country", "type": "nindex", ...}, <-- entity reference!
324
+ # {"name": "total_revenue", "type": "float", ...},
325
+ # {"name": "ticker_symbol", "type": "string", ...},
326
+ # {"name": "industry", "type": "nindex", ...}, <-- entity reference!
327
+ # ]
328
+ ```
329
+
330
+ ### Step 2: Build a type map (once per session)
331
+
332
+ ```python
333
+ def build_property_type_map(schema_result: dict) -> dict[str, str]:
334
+ """Map property names to their types from schema."""
335
+ type_map = {}
336
+ for prop in schema_result.get("properties", []):
337
+ type_map[prop["name"]] = prop["type"]
338
+ return type_map
339
+
340
+ # Cache this — don't re-fetch schema for every tool call
341
+ property_types = build_property_type_map(schema)
342
+ # {"country": "nindex", "total_revenue": "float", "ticker_symbol": "string", ...}
343
+ ```
344
+
345
+ ### Step 3: Resolve reference values before display
346
+
347
+ ```python
348
+ async def format_properties(
349
+ properties: dict,
350
+ property_types: dict[str, str],
351
+ mcp_call,
352
+ ) -> dict[str, str]:
353
+ """Format property values for user-facing display."""
354
+ formatted = {}
355
+ for name, prop in properties.items():
356
+ value = prop["value"]
357
+ prop_type = property_types.get(name, "string")
358
+
359
+ if prop_type == "nindex" and isinstance(value, (str, int)):
360
+ # This is an entity reference — resolve to display name
361
+ resolved = await mcp_call("elemental_get_entity", {
362
+ "entity_id": {"id": str(value), "id_type": "neid"}
363
+ })
364
+ entity = resolved.get("entity")
365
+ formatted[name] = entity["name"] if entity else str(value)
366
+ elif isinstance(value, (int, float)) and abs(value) >= 1_000_000:
367
+ formatted[name] = _format_large_number(value)
368
+ else:
369
+ formatted[name] = str(value)
370
+ return formatted
371
+
372
+ def _format_large_number(n: float) -> str:
373
+ """Format large numbers: 52900000000 -> '$52.9B'."""
374
+ abs_n = abs(n)
375
+ if abs_n >= 1e12:
376
+ return f"${n/1e12:.1f}T"
377
+ if abs_n >= 1e9:
378
+ return f"${n/1e9:.1f}B"
379
+ if abs_n >= 1e6:
380
+ return f"${n/1e6:.1f}M"
381
+ return f"${n:,.0f}"
382
+ ```
383
+
384
+ ### Property type values you'll encounter
385
+
386
+ | Schema type | Value is | How to display |
387
+ |---|---|---|
388
+ | `string` | Plain text | Display directly |
389
+ | `integer`, `float` | Number | Format with units (check `unit` in schema) |
390
+ | `nindex` | Entity NEID | **Must resolve** via `elemental_get_entity` |
391
+ | `boolean` | `true`/`false` | Display as Yes/No |
392
+ | `datetime` | ISO 8601 string | Format as human-readable date |
393
+
394
+ ---
395
+
396
+ ## Common Patterns
397
+
398
+ ### Resolve and cache an entity
399
+
400
+ ```python
401
+ async def resolve_entity(
402
+ name: str,
403
+ session_state: dict,
404
+ mcp_call,
405
+ neid: str | None = None,
406
+ ) -> dict | None:
407
+ """Resolve entity, using cache if available."""
408
+ cache = session_state.setdefault("entities", {})
409
+
410
+ # Check cache by NEID or name
411
+ cache_key = neid or name.lower()
412
+ if cache_key in cache:
413
+ return cache[cache_key]
414
+
415
+ params = {}
416
+ if neid:
417
+ params["entity_id"] = {"id": neid, "id_type": "neid"}
418
+ else:
419
+ params["entity"] = name
420
+
421
+ result = await mcp_call("elemental_get_entity", params)
422
+ entity = result.get("entity")
423
+ if not entity:
424
+ return None
425
+
426
+ # Cache by both NEID and lowercase name
427
+ cache[entity["neid"]] = entity
428
+ cache[entity["name"].lower()] = entity
429
+ return entity
430
+ ```
431
+
432
+ ### Build a rich entity briefing
433
+
434
+ Don't return a one-paragraph summary. Chain multiple calls to build a
435
+ comprehensive report:
436
+
437
+ ```python
438
+ async def entity_briefing(name: str, mcp_call, session_state: dict) -> str:
439
+ """Build a comprehensive entity briefing."""
440
+ # 1. Resolve + properties
441
+ entity = await mcp_call("elemental_get_entity", {
442
+ "entity": name,
443
+ "properties": [
444
+ "country", "ticker_symbol", "total_revenue", "net_income",
445
+ "total_assets", "industry", "lei", "company_cik"
446
+ ]
447
+ })
448
+ if not entity.get("entity"):
449
+ return f"Could not resolve entity: {name}"
450
+
451
+ e = entity["entity"]
452
+ neid = e["neid"]
453
+ report_parts = [f"# {e['name']}", f"Type: {e.get('flavor', 'unknown')}"]
454
+
455
+ # 2. Format properties (handling nindex resolution)
456
+ if e.get("properties"):
457
+ schema = await mcp_call("elemental_get_schema", {"flavor": e.get("flavor", "")})
458
+ ptypes = build_property_type_map(schema)
459
+ props = await format_properties(e["properties"], ptypes, mcp_call)
460
+ for k, v in props.items():
461
+ report_parts.append(f"- {k}: {v}")
462
+
463
+ # 3. Key relationships
464
+ for related_flavor, label in [("person", "Key People"), ("organization", "Related Orgs")]:
465
+ related = await mcp_call("elemental_get_related", {
466
+ "entity_id": {"id": neid, "id_type": "neid"},
467
+ "related_flavor": related_flavor,
468
+ "limit": 10
469
+ })
470
+ if related.get("relationships"):
471
+ report_parts.append(f"\n## {label}")
472
+ for r in related["relationships"]:
473
+ types = ", ".join(r.get("relationship_types", []))
474
+ report_parts.append(f"- {r['name']} ({types})")
475
+
476
+ # 4. Recent events
477
+ events = await mcp_call("elemental_get_events", {
478
+ "entity_id": {"id": neid, "id_type": "neid"},
479
+ "limit": 10
480
+ })
481
+ if events.get("events"):
482
+ report_parts.append("\n## Recent Events")
483
+ for ev in events["events"]:
484
+ props = ev.get("properties", {})
485
+ date = props.get("date", {}).get("value", "")
486
+ cat = props.get("category", {}).get("value", "")
487
+ desc = props.get("description", {}).get("value", ev["name"])
488
+ report_parts.append(f"- [{date}] {cat}: {desc}")
489
+
490
+ return "\n".join(report_parts)
491
+ ```
492
+
493
+ ### Fetch events correctly
494
+
495
+ Always use `elemental_get_events`. Never try to find events by scanning
496
+ property names or PID names for keywords like "event" or "filing".
497
+
498
+ ```python
499
+ # CORRECT — use the dedicated events tool
500
+ events = await mcp_call("elemental_get_events", {
501
+ "entity": "Intel",
502
+ "categories": ["Regulatory Action", "Acquisition"],
503
+ "time_range": {"after": "2025-01-01"},
504
+ "include_participants": True
505
+ })
506
+
507
+ # WRONG — do NOT scan properties/PIDs for event-like names
508
+ # This matches "filed" (a document relationship), not actual events
509
+ schema = await mcp_call("elemental_get_schema", {"flavor": "organization"})
510
+ event_pids = [p for p in schema["properties"] if "event" in p["name"]] # BAD
511
+ ```
512
+
513
+ ### Fetch related entities with properties
514
+
515
+ ```python
516
+ # Get board members with their titles and nationalities
517
+ board = await mcp_call("elemental_get_related", {
518
+ "entity": "JPMorgan Chase",
519
+ "related_flavor": "person",
520
+ "relationship_types": ["board_member_of", "is_officer"],
521
+ "related_properties": ["title", "nationality"],
522
+ "limit": 30
523
+ })
524
+
525
+ for person in board.get("relationships", []):
526
+ name = person["name"]
527
+ title = person.get("properties", {}).get("title", {}).get("value", "")
528
+ print(f"{name} — {title}")
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Common Properties & Relationships Quick Reference
534
+
535
+ Use `elemental_introspect` to discover what's actually populated for a
536
+ given flavor. Use `elemental_get_schema` for the full property list. The
537
+ tables below are a starting-point cheat sheet — not exhaustive.
538
+
539
+ ### Properties by flavor
540
+
541
+ | Flavor | Common properties |
542
+ |---|---|
543
+ | `organization` | `country` (nindex), `ticker_symbol`, `total_revenue`, `net_income`, `total_assets`, `industry` (nindex), `lei`, `company_cik`, `ein`, `website` |
544
+ | `person` | `nationality` (nindex), `title`, `birth_date`, `gender` |
545
+ | `government_body` | `country` (nindex), `jurisdiction` |
546
+ | `article` | `headline`, `published_date`, `source`, `url` |
547
+ | `event` | `category`, `date`, `description`, `likelihood` |
548
+ | `financial_instrument` | `ticker_symbol`, `exchange`, `currency` |
549
+
550
+ Properties marked `(nindex)` are entity references — their raw value is
551
+ a NEID that must be resolved to a display name. See "The Property Type
552
+ Problem" above.
553
+
554
+ ### Relationship types and direction
555
+
556
+ The `direction` parameter on `elemental_get_related` controls traversal.
557
+ Getting it wrong returns zero results with no error.
558
+
559
+ | Relationship | Meaning | Direction from center |
560
+ |---|---|---|
561
+ | `board_member_of` | Person sits on org's board | `"incoming"` when center is org |
562
+ | `is_officer` | Person is an officer of org | `"incoming"` when center is org |
563
+ | `subsidiary_of` | Org is a subsidiary of parent | `"outgoing"` from subsidiary |
564
+ | `owns` | Entity owns another entity | `"outgoing"` from owner |
565
+ | `appears_in` | Entity mentioned in article | `"both"` is usually safest |
566
+ | `participant` | Entity participates in event | `"both"` is usually safest |
567
+ | `filed` | Org filed a document | `"outgoing"` from org |
568
+ | `works_at` | Person works at org | `"outgoing"` from person |
569
+
570
+ > **Tip:** If you're unsure about direction, use `"both"` (the default)
571
+ > first and check the results. Then narrow to `"incoming"` or
572
+ > `"outgoing"` once you know which way the edges point. You can also use
573
+ > `elemental_get_relationships` to see all relationship types and counts
574
+ > between two specific entities.
575
+
576
+ > **Tip:** Use `elemental_introspect(flavor="organization")` to see which
577
+ > properties and relationships are actually populated with data and their
578
+ > fill rates. This prevents building features against empty data.
579
+
580
+ ---
581
+
582
+ ## Anti-Patterns
583
+
584
+ These are mistakes previous agents have made. Do not repeat them.
585
+
586
+ 1. **Do not substring-match PID names to find events or filings.**
587
+ PIDs like `"filed"` are document relationship IDs, not event timestamps.
588
+ Use `elemental_get_events` for events.
589
+
590
+ 2. **Do not render raw NEID values in user-facing text.** If a property
591
+ value looks like a large number (`5816460566439750832`), it's probably
592
+ a `nindex` reference. Check the schema.
593
+
594
+ 3. **Do not skip schema lookup.** Property types are not guessable from
595
+ names alone. `"country"` looks like it should be a string, but it's
596
+ an `nindex` (entity reference). Always call `elemental_get_schema`
597
+ at least once per flavor.
598
+
599
+ 4. **Do not return thin briefings.** When a user asks "tell me about
600
+ Intel," they expect a comprehensive research report — not a single
601
+ paragraph. Chain entity + related + events into a thorough response.
602
+
603
+ 5. **Do not fabricate citation refs.** Only include `[ref_...]` markers
604
+ when the `ref` field is actually present in the tool response data.
605
+ The client renders these as numbered citations with source links.
606
+
607
+ ---
608
+
609
+ ## Citation Handling
610
+
611
+ Property values may include a `ref` field (e.g. `"ref_a3f2b1c8"`). When
612
+ building user-facing text, include the ref in brackets after the fact:
613
+
614
+ ```
615
+ Revenue was $52.9B [ref_a3f2b1c8]
616
+ ```
617
+
618
+ The chat UI translates these into numbered source citations. Rules:
619
+ - Only include refs that are present in the actual response data
620
+ - Copy the ref string exactly — never construct or modify refs
621
+ - Omit the bracket when no ref is present for a value
@@ -6,7 +6,13 @@ globs: agents/**
6
6
 
7
7
  # Agents: Elemental MCP (MCP-only)
8
8
 
9
- In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the knowledge graph. **`broadchurch_auth` HTTP** to the Query Server is the **api-mcp** path; here you wire **MCP tools** into ADK (e.g. **McpToolset** + `SseConnectionParams` to your Elemental MCP URL — from env or `broadchurch.yaml` / gateway).
9
+ In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the knowledge graph. **`broadchurch_auth` HTTP** to the Query Server is the **api-mcp** path; here you wire **MCP tools** into ADK via **McpToolset** + **`StreamableHTTPConnectionParams`** to your Elemental MCP URL — from env or `broadchurch.yaml` / gateway.
10
+
11
+ > **Transport class:** The Elemental MCP server uses **Streamable HTTP**
12
+ > transport. Use `StreamableHTTPConnectionParams` — **NOT**
13
+ > `SseConnectionParams`. Using `SseConnectionParams` will silently fail:
14
+ > the agent starts with zero tools and the LLM hallucinates code. See the
15
+ > "Wiring McpToolset" section below for a working snippet.
10
16
 
11
17
  ## Primary agent patterns (all first-class)
12
18
 
@@ -18,10 +24,52 @@ In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the know
18
24
 
19
25
  You can deploy **multiple agents** with different mixes of A–C.
20
26
 
27
+ ## Wiring McpToolset
28
+
29
+ Use `StreamableHTTPConnectionParams` to connect to the Elemental MCP
30
+ server. Resolve the URL from `broadchurch.yaml` (gateway-proxied in
31
+ production) or fall back to `ELEMENTAL_MCP_URL` for local dev.
32
+
33
+ ```python
34
+ import os
35
+ from pathlib import Path
36
+ import yaml
37
+ from google.adk.tools.mcp_tool import McpToolset
38
+ from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
39
+
40
+ def _get_mcp_url(server_name: str = "elemental") -> str:
41
+ """Resolve MCP server URL from env or broadchurch.yaml."""
42
+ env_url = os.environ.get("ELEMENTAL_MCP_URL")
43
+ if env_url:
44
+ return env_url
45
+ for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
46
+ if candidate.exists():
47
+ config = yaml.safe_load(candidate.read_text()) or {}
48
+ gw = config.get("gateway", {})
49
+ org_id = config.get("tenant", {}).get("org_id", "")
50
+ if gw.get("url") and org_id:
51
+ return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
52
+ return config.get("mcp", {}).get(server_name, "")
53
+ return ""
54
+
55
+ mcp_url = _get_mcp_url()
56
+ mcp_tools = [McpToolset(connection_params=StreamableHTTPConnectionParams(url=mcp_url))] if mcp_url else []
57
+ ```
58
+
59
+ > **Silent failure warning:** If `McpToolset` cannot connect (wrong
60
+ > transport, bad URL, server down), the agent starts with **zero MCP
61
+ > tools** and the LLM will hallucinate code instead of calling tools.
62
+ > Always verify tool count at startup:
63
+ >
64
+ > ```python
65
+ > if not mcp_url:
66
+ > raise RuntimeError("No MCP URL configured — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
67
+ > ```
68
+
21
69
  ## Python stack (typical)
22
70
 
23
71
  - `google-adk` — agent framework
24
- - MCP client or ADK **McpToolset** — `ELEMENTAL_MCP_URL` or gateway-proxied MCP URL
72
+ - ADK **McpToolset** + **`StreamableHTTPConnectionParams`** — `ELEMENTAL_MCP_URL` or gateway-proxied MCP URL
25
73
  - **`psycopg2-binary`** — only when an agent **persists** to `DATABASE_URL`
26
74
 
27
75
  ```bash