@yottagraph-app/aether-instructions 1.1.33 → 1.1.35

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.35",
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
 
@@ -52,7 +52,32 @@ Each SEC form type is a separate flavor, namespaced under `sec`. All share the s
52
52
  | `sec::13f_hr` | 13F-HR | Institutional investment manager holdings |
53
53
  | `sec::def_14a` | DEF 14A | Definitive proxy statement |
54
54
 
55
- Sub-records (8-K events, Form 4 transactions, Form 3 holdings) use their parent filing's flavor.
55
+ ### Sub-Records (8-K Events, Form 4 Transactions, Form 3 Holdings)
56
+
57
+ Sub-records are **separate graph entities**, not nested properties on the parent filing. Each sub-record has its own NEID and can be queried independently. They use the same flavor as their parent filing (e.g., `sec::8_k` for 8-K events, `sec::form_4` for Form 4 transactions).
58
+
59
+ **Entity naming pattern:**
60
+ - 8-K events: `{accession_number}_evt_{n}` (e.g., `0000320193-24-000067_evt_1`)
61
+ - Form 4 transactions: `{accession_number}_trx_{n}` (e.g., `0000320193-24-000067_trx_1`)
62
+ - Form 3 holdings: `{accession_number}_holding_{n}`
63
+
64
+ **Relationships on sub-records:**
65
+ - `filed` — points to the parent filing's accession number
66
+ - `issued_by` — points to the company
67
+
68
+ **Critical querying note:** Properties like `form_8k_event`, `form_8k_item_code`, `transaction_type`, `shares_transacted`, and other sub-record-specific properties do NOT appear on the parent filing entity or the organization entity. You must traverse the graph to the sub-record entities to access them.
69
+
70
+ **Traversal path:**
71
+ ```
72
+ organization --[filed]--> 8-K filing --[linked, distance 1]--> event sub-records
73
+ ```
74
+
75
+ **Example: Finding 8-K event sub-records for a company:**
76
+ 1. Get filing NEIDs from the organization's `filed` property
77
+ 2. Filter to 8-K filings by checking `form_type == "8-K"`
78
+ 3. For each 8-K filing, use a `linked` expression (distance 1, direction both) to find connected entities
79
+ 4. Exclude the organization and the filing itself from results — the remaining entities are the event sub-records
80
+ 5. Query those sub-record NEIDs for `form_8k_event` and `form_8k_item_code`
56
81
 
57
82
  ### `person`
58
83
 
@@ -161,12 +186,34 @@ The six core financial properties appear on both the **organization** and its **
161
186
 
162
187
  #### 8-K Corporate Events (source: `edgar_8k`)
163
188
 
164
- Data source: 8-K current report filings. Properties on document sub-records.
189
+ Data source: 8-K current report filings.
190
+
191
+ > **Important:** These properties are on **event sub-record entities**, not on the parent 8-K filing or the organization. See the [Sub-Records section](#sub-records-8-k-events-form-4-transactions-form-3-holdings) above for how to find them.
165
192
 
193
+ **Core event properties:**
166
194
  * `form_8k_event` — Snake_case event identifier. Examples: `"material_agreement"`, `"officer_director_change"`
167
195
  * `form_8k_item_code` — Raw SEC item number. Examples: `"1.01"`, `"5.02"`
196
+ * `event_severity` — Event importance classification. Values: `"critical"`, `"high"`, `"medium"`, `"low"`
168
197
  * `category` — Sub-classification of Item 8.01 Other Events. Examples: `"cybersecurity_incident"`
169
198
 
199
+ **Sub-record identity and relationships:**
200
+ * `accession_number` — Synthetic sub-record identifier. Example: `"0000320193-24-000067_evt_1"`
201
+ * `filed` — Relationship: event sub-record → parent 8-K filing
202
+ * `issued_by` — Relationship: event sub-record → company
203
+
204
+ **Item 8.01 keyword flags** (set to `"true"` when matched):
205
+ * `8k_cybersecurity_keyword` — Cybersecurity-related disclosure detected
206
+ * `8k_litigation_keyword` — Litigation-related disclosure detected
207
+ * `8k_regulatory_keyword` — Regulatory-related disclosure detected
208
+ * `8k_operational_keyword` — Operational issue disclosure detected
209
+
210
+ **ABS event flags** (Items 6.01–6.05, set to `"true"` when applicable):
211
+ * `abs_servicing_event` — Item 6.01: ABS servicing event
212
+ * `abs_servicer_change` — Item 6.02: ABS servicer change
213
+ * `abs_credit_enhancement_change` — Item 6.03: ABS credit enhancement change
214
+ * `abs_failure_event` — Item 6.04: ABS failure to make distribution
215
+ * `abs_securities_act` — Item 6.05: ABS Securities Act updating disclosure
216
+
170
217
  #### Beneficial Ownership (source: `edgar_sc_13d`, `edgar_sc_13g`)
171
218
 
172
219
  Data source: SC 13D/G XML filings. Properties on the filer organization.
@@ -240,8 +287,14 @@ Data source: Form 3/4 XML.
240
287
 
241
288
  #### Form 4 Transactions (source: `edgar_4`)
242
289
 
243
- Properties on document sub-records, one per transaction.
290
+ > **Important:** These properties are on **transaction sub-record entities**, not on the parent Form 4 filing. See the [Sub-Records section](#sub-records-8-k-events-form-4-transactions-form-3-holdings) above for how to find them.
291
+
292
+ **Sub-record identity and relationships:**
293
+ * `accession_number` — Synthetic sub-record identifier. Example: `"0000320193-24-000067_trx_1"`
294
+ * `filed` — Relationship: transaction sub-record → parent Form 4 filing
295
+ * `issued_by` — Relationship: transaction sub-record → issuer company
244
296
 
297
+ **Transaction properties:**
245
298
  * `transaction_type` — Human-readable code description. Examples: `"Open market or private purchase"`, `"Grant, award, or other acquisition"`
246
299
  * `transaction_date` — Transaction date (YYYY-MM-DD)
247
300
  * `acquired_disposed_code` — `"A"` (acquired) or `"D"` (disposed)
@@ -284,6 +337,8 @@ Data source: 13F-HR XML information table.
284
337
 
285
338
  ## Entity Relationships
286
339
 
340
+ > **Sub-record traversal:** Sub-records (8-K events, Form 4 transactions, Form 3 holdings) are separate document entities. To find them, traverse from the parent filing using a `linked` expression (distance 1). The sub-record's `filed` property points back to the parent filing, and `issued_by` points to the company.
341
+
287
342
  ```
288
343
  organization ──[filed]────────────────────→ document
289
344
  organization ──[filing_reference]─────────→ document
@@ -296,7 +351,8 @@ document ──[refers_to]──────────────
296
351
  document ──[filer]────────────────────→ organization (SC 13D/G)
297
352
  document ──[group_member]─────────────→ organization (SC 13D)
298
353
  document ──[compares_to]──────────────→ organization (DEF 14A)
299
- document ──[filed]────────────────────→ document (sub-record → parent)
354
+ document (sub-record)──[filed]────────────────────→ document (8-K event → parent 8-K filing)
355
+ document (sub-record)──[issued_by]────────────────→ organization (8-K event / Form 4 txn → company)
300
356
  person ──[is_officer]───────────────→ organization
301
357
  person ──[is_director]──────────────→ organization
302
358
  person ──[is_ten_percent_owner]─────→ organization
@@ -126,6 +126,14 @@ flavors:
126
126
  strong_id_properties: ["accession_number"]
127
127
  passive: true
128
128
 
129
+ - name: "filing"
130
+ namespace: "sec"
131
+ description: "SEC filing (generic, for form types without a specific modeled flavor)"
132
+ display_name: "SEC Filing"
133
+ mergeability: not_mergeable
134
+ strong_id_properties: ["accession_number"]
135
+ passive: true
136
+
129
137
  - name: "person"
130
138
  description: "A real person as opposed to a fictional character, such as a CEO, politician, or public figure"
131
139
  display_name: "Person"
@@ -140,6 +148,13 @@ flavors:
140
148
  strong_id_properties: ["cusip_number"]
141
149
  passive: true
142
150
 
151
+ - name: "industry"
152
+ description: "A Standard Industrial Classification (SIC) industry category assigned by the SEC"
153
+ display_name: "Industry"
154
+ mergeability: not_mergeable
155
+ strong_id_properties: ["sic_code"]
156
+ passive: true
157
+
143
158
  # =============================================================================
144
159
  # PROPERTIES — Organization
145
160
  # =============================================================================
@@ -176,7 +191,7 @@ properties:
176
191
  description: "Four-digit Standard Industrial Classification code"
177
192
  display_name: "SIC Code"
178
193
  mergeability: not_mergeable
179
- domain_flavors: ["organization"]
194
+ domain_flavors: ["organization", "industry"]
180
195
  passive: true
181
196
 
182
197
  - name: "sic_description"
@@ -184,7 +199,7 @@ properties:
184
199
  description: "Human-readable SIC code description"
185
200
  display_name: "SIC Description"
186
201
  mergeability: not_mergeable
187
- domain_flavors: ["organization"]
202
+ domain_flavors: ["organization", "industry"]
188
203
  passive: true
189
204
 
190
205
  - name: "state_of_incorporation"
@@ -574,7 +589,7 @@ properties:
574
589
  description: "SEC accession number uniquely identifying a filing"
575
590
  display_name: "Accession Number"
576
591
  mergeability: not_mergeable
577
- domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
592
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
578
593
  passive: true
579
594
 
580
595
  - name: "form_type"
@@ -582,7 +597,7 @@ properties:
582
597
  description: "Normalized SEC form type (e.g. 10-K, SC 13D, 4)"
583
598
  display_name: "Form Type"
584
599
  mergeability: not_mergeable
585
- domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
600
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
586
601
  passive: true
587
602
 
588
603
  - name: "filing_date"
@@ -590,7 +605,7 @@ properties:
590
605
  description: "Date the filing was submitted to the SEC (YYYY-MM-DD)"
591
606
  display_name: "Filing Date"
592
607
  mergeability: not_mergeable
593
- domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
608
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
594
609
  passive: true
595
610
 
596
611
  - name: "report_date"
@@ -598,7 +613,7 @@ properties:
598
613
  description: "End date of the primary reporting period (YYYY-MM-DD)"
599
614
  display_name: "Report Date"
600
615
  mergeability: not_mergeable
601
- domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
616
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
602
617
  passive: true
603
618
 
604
619
  # ── XBRL Financial Facts (key concepts) ──
@@ -1717,11 +1732,19 @@ relationships:
1717
1732
  # ── Organization → Filing ──
1718
1733
 
1719
1734
  - name: "filed"
1720
- description: "Link from a company to an SEC filing document it filed, or from a sub-record (event, transaction, holding) to its parent filing"
1735
+ description: "Link from a company or person to an SEC filing document they filed, or from a sub-record (event, transaction, holding) to its parent filing"
1721
1736
  display_name: "Filed"
1722
1737
  mergeability: not_mergeable
1723
- domain_flavors: ["organization", "sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
1724
- target_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
1738
+ domain_flavors: ["organization", "person", "sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
1739
+ target_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
1740
+ passive: true
1741
+
1742
+ - name: "is_issuer_of"
1743
+ description: "Link from an issuer organization to an ownership filing (Forms 3/4, SC 13D/G) where the CIK is the issuer, not the filer"
1744
+ display_name: "Is Issuer Of"
1745
+ mergeability: not_mergeable
1746
+ domain_flavors: ["organization"]
1747
+ target_flavors: ["sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::filing"]
1725
1748
  passive: true
1726
1749
 
1727
1750
  - name: "filing_reference"
@@ -1735,10 +1758,10 @@ relationships:
1735
1758
  # ── Filing → Organization ──
1736
1759
 
1737
1760
  - name: "issued_by"
1738
- description: "Link from a filing to the company it pertains to (the filer for most forms; the issuer for ownership forms)"
1761
+ description: "Link from a filing or financial instrument to the company it pertains to (the filer for most forms; the issuer for ownership forms and 13F holdings)"
1739
1762
  display_name: "Issued By"
1740
1763
  mergeability: not_mergeable
1741
- domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a"]
1764
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing", "financial_instrument"]
1742
1765
  target_flavors: ["organization"]
1743
1766
  passive: true
1744
1767
 
@@ -1751,10 +1774,10 @@ relationships:
1751
1774
  passive: true
1752
1775
 
1753
1776
  - name: "filer"
1754
- description: "Link from a filing to the beneficial owner or manager who filed it"
1777
+ description: "Link from a filing to the entity (person or organization) who filed it"
1755
1778
  display_name: "Filer"
1756
1779
  mergeability: not_mergeable
1757
- domain_flavors: ["sec::sc_13d", "sec::sc_13g", "sec::13f_hr"]
1780
+ domain_flavors: ["sec::10_k", "sec::10_q", "sec::20_f", "sec::8_k", "sec::6_k", "sec::40_f", "sec::form_3", "sec::form_4", "sec::sc_13d", "sec::sc_13g", "sec::13f_hr", "sec::def_14a", "sec::filing"]
1758
1781
  target_flavors: ["organization", "person"]
1759
1782
  passive: true
1760
1783
 
@@ -1803,10 +1826,10 @@ relationships:
1803
1826
  # ── Organization → Financial Instrument ──
1804
1827
 
1805
1828
  - name: "holds_position"
1806
- description: "Link from an investment manager to a security it holds (13F-HR)"
1829
+ description: "Link from an investment manager or fund to a security it holds (13F-HR, N-PORT)"
1807
1830
  display_name: "Holds Position"
1808
1831
  mergeability: not_mergeable
1809
- domain_flavors: ["organization"]
1832
+ domain_flavors: ["organization", "financial_instrument"]
1810
1833
  target_flavors: ["financial_instrument"]
1811
1834
  passive: true
1812
1835
 
@@ -1844,6 +1867,16 @@ relationships:
1844
1867
  target_flavors: ["organization"]
1845
1868
  passive: true
1846
1869
 
1870
+ # ── Organization → Financial Instrument (issuer link) ──
1871
+
1872
+ - name: "issues_security"
1873
+ description: "Link from an issuing company to a financial instrument it has issued, identified by CUSIP (SC 13D/G)"
1874
+ display_name: "Issues Security"
1875
+ mergeability: not_mergeable
1876
+ domain_flavors: ["organization"]
1877
+ target_flavors: ["financial_instrument"]
1878
+ passive: true
1879
+
1847
1880
  # =============================================================================
1848
1881
  # ATTRIBUTES
1849
1882
  # =============================================================================
@@ -1891,3 +1924,61 @@ attributes:
1891
1924
  description: "Fiscal period: FY (annual), Q1, Q2, Q3 (quarterly)"
1892
1925
  display_name: "Filing Period"
1893
1926
  mergeability: not_mergeable
1927
+
1928
+ # ── holds_position relationship attributes (13F-HR) ──
1929
+
1930
+ - property: "holds_position"
1931
+ name: "position_value"
1932
+ type: string
1933
+ description: "Market value of the position in thousands of USD"
1934
+ display_name: "Position Value"
1935
+ mergeability: not_mergeable
1936
+
1937
+ - property: "holds_position"
1938
+ name: "shares_held"
1939
+ type: string
1940
+ description: "Number of shares or principal amount held"
1941
+ display_name: "Shares Held"
1942
+ mergeability: not_mergeable
1943
+
1944
+ - property: "holds_position"
1945
+ name: "instrument_type"
1946
+ type: string
1947
+ description: "Type of instrument: SH (shares) or PRN (principal amount)"
1948
+ display_name: "Instrument Type"
1949
+ mergeability: not_mergeable
1950
+
1951
+ - property: "holds_position"
1952
+ name: "put_call"
1953
+ type: string
1954
+ description: "Option type if applicable: PUT or CALL"
1955
+ display_name: "Put/Call"
1956
+ mergeability: not_mergeable
1957
+
1958
+ - property: "holds_position"
1959
+ name: "investment_discretion"
1960
+ type: string
1961
+ description: "Investment discretion: SOLE, SHARED, or DEFINED"
1962
+ display_name: "Investment Discretion"
1963
+ mergeability: not_mergeable
1964
+
1965
+ - property: "holds_position"
1966
+ name: "voting_authority_sole"
1967
+ type: string
1968
+ description: "Shares with sole voting authority"
1969
+ display_name: "Voting Authority (Sole)"
1970
+ mergeability: not_mergeable
1971
+
1972
+ - property: "holds_position"
1973
+ name: "voting_authority_shared"
1974
+ type: string
1975
+ description: "Shares with shared voting authority"
1976
+ display_name: "Voting Authority (Shared)"
1977
+ mergeability: not_mergeable
1978
+
1979
+ - property: "holds_position"
1980
+ name: "voting_authority_none"
1981
+ type: string
1982
+ description: "Shares with no voting authority"
1983
+ display_name: "Voting Authority (None)"
1984
+ mergeability: not_mergeable
@@ -48,7 +48,7 @@ A traded security, index, or reference rate for which FRED publishes price or yi
48
48
  - Primary key: entity name (e.g. "10-Year U.S. Treasury", "S&P 500"). No strong ID — resolved by name with MERGEABLE mergeability.
49
49
  - Entity resolver: named entity, MERGEABLE. Disambiguation snippet includes series title, frequency, and units.
50
50
  - Source: `fred-source`
51
- - Entities produced: U.S. Treasuries (1Y, 2Y, 10Y, 30Y, 3M bill), SOFR, SONIA, S&P 500, NASDAQ Composite, CBOE VIX, ICE BofA High Yield Index, FX pairs (JPY/USD, KRW/USD, USD/EUR, USD/GBP, CAD/USD, CNY/USD), U.S. Dollar Index, U.S. Dollar Advanced Economy Index
51
+ - Entities produced: U.S. Treasuries (1Y, 2Y, 10Y, 30Y, 3M bill), SOFR, SONIA, NASDAQ Composite, CBOE VIX, FX pairs (JPY/USD, KRW/USD, USD/EUR, USD/GBP, CAD/USD, CNY/USD), U.S. Dollar Index, U.S. Dollar Advanced Economy Index
52
52
 
53
53
  ### `product`
54
54
 
@@ -77,7 +77,7 @@ These atoms appear once per series record, timestamped at the first observation
77
77
  * `fred_series_id`
78
78
  * Definition: FRED's unique alphanumeric identifier for the series.
79
79
  * Source flavor: location, organization, financial_instrument, product
80
- * Examples: `"GDP"`, `"UNRATE"`, `"DGS10"`, `"SP500"`
80
+ * Examples: `"GDP"`, `"UNRATE"`, `"DGS10"`
81
81
  * Derivation: `id` field from the FRED `/series` API response.
82
82
 
83
83
  * `notes`
@@ -198,7 +198,6 @@ Each observation in a series becomes one atom timestamped at the observation dat
198
198
  * `bank_lending_standards` — Net % of banks tightening C&I loan standards for large firms (Percent, quarterly). Series: DRTSCILM.
199
199
  * `bank_lending_standards_small` — Net % of banks tightening C&I loan standards for small firms (Percent, quarterly). Series: DRTSCLNM. **Note: FRED reports this series as discontinued/nonexistent as of 2026-03.**
200
200
  * `bank_lending_standards_cre` — Net % of banks tightening CRE lending standards (Percent, quarterly). Series: DRTSRCL. **Note: FRED reports this series as discontinued/nonexistent as of 2026-03.**
201
- * `baa_10y_spread` — Moody's Baa corporate bond yield minus 10Y Treasury (Percent, daily). Series: BAA10Y.
202
201
  * `credit_card_delinquency_rate` — Delinquency rate on credit card loans, all commercial banks (Percent, quarterly). Series: DRCCLACBS.
203
202
  * `cre_delinquency_rate` — Delinquency rate on CRE loans excluding farmland (Percent, quarterly). Series: DRCRELEXFACBS.
204
203
 
@@ -215,8 +214,6 @@ Each observation in a series becomes one atom timestamped at the observation dat
215
214
  * `building_permits` — New privately-owned housing units authorized, SAAR (Thousands of Units, monthly). Series: PERMIT.
216
215
  * `housing_months_supply` — Monthly supply of new houses, seasonally adjusted (Months' Supply, monthly). Series: MSACSR.
217
216
  * `home_price_median` — Median sales price of houses sold (Dollars, quarterly). Series: MSPUS.
218
- * `home_price_index` — S&P CoreLogic Case-Shiller U.S. National Home Price Index (Index Jan 2000=100, monthly). Series: CSUSHPINSA.
219
- * `home_price_index_20city` — S&P CoreLogic Case-Shiller 20-City Composite Home Price Index (Index Jan 2000=100, monthly). Series: SPCS20RSA.
220
217
  * `home_price_index_fhfa` — FHFA all-transactions house price index (Index 1980:Q1=100, quarterly). Series: USSTHPI.
221
218
  * `mortgage_rate_30y` — 30-year fixed rate mortgage average (Percent, weekly). Series: MORTGAGE30US.
222
219
  * `rental_vacancy_rate` — Rental housing vacancy rate (Percent, quarterly). Series: RRVRUSQ156N.
@@ -286,8 +283,7 @@ Each observation in a series becomes one atom timestamped at the observation dat
286
283
  #### Financial Markets (financial_instrument)
287
284
 
288
285
  * `yield` — Market yield or interest rate for a fixed-income instrument (Percent). Series: DGS1, DGS2, DGS10, DGS30, DTB3, TB3MS, SOFR, IUDSOIA.
289
- * `price` — Market price, index level, or exchange rate (varies by instrument). Series: SP500, NASDAQCOM, VIXCLS, DTWEXBGS, TWEXAFEGSMTH, DEXUSEU, DEXUSUK, DEXJPUS, DEXKOUS, EXCAUS, DEXCHUS.
290
- * `option_adjusted_spread` — Option-adjusted spread for a bond index (Percent, daily). Series: BAMLH0A0HYM2.
286
+ * `price` — Market price, index level, or exchange rate (varies by instrument). Series: NASDAQCOM, VIXCLS, DTWEXBGS, TWEXAFEGSMTH, DEXUSEU, DEXUSUK, DEXJPUS, DEXKOUS, EXCAUS, DEXCHUS.
291
287
 
292
288
  #### Commodity Prices (product)
293
289
 
@@ -308,6 +304,6 @@ The FRED source produces no `data_nindex` relationship properties. All atoms are
308
304
  ```
309
305
  location ── [fed_funds_rate, gdp, unemployment_rate, cpi, ...] (scalar observation properties)
310
306
  organization ── [fed_funds_rate, policy_rate, monetary_base, ...]
311
- financial_instrument ── [yield, price, option_adjusted_spread]
307
+ financial_instrument ── [yield, price]
312
308
  product ── [price]
313
309
  ```
@@ -77,7 +77,7 @@ properties:
77
77
  display_name: "FRED Series ID"
78
78
  mergeability: not_mergeable
79
79
  domain_flavors: ["fred_series"]
80
- examples: ["GDP", "UNRATE", "DGS10", "SP500"]
80
+ examples: ["GDP", "UNRATE", "DGS10"]
81
81
  passive: true
82
82
 
83
83
  - name: "name"
@@ -86,7 +86,7 @@ properties:
86
86
  display_name: "Name"
87
87
  mergeability: not_mergeable
88
88
  domain_flavors: ["fred_series"]
89
- examples: ["Gross Domestic Product", "Federal Funds Effective Rate", "S&P 500"]
89
+ examples: ["Gross Domestic Product", "Federal Funds Effective Rate"]
90
90
  passive: true
91
91
 
92
92
  - name: "notes"
@@ -0,0 +1,64 @@
1
+ # Dataset schema for SIC industry classifications derived from SEC EDGAR filings.
2
+ #
3
+ # This schema introduces the `industry` flavor — a Standard Industrial
4
+ # Classification (SIC) category identified by its 4-digit code. Organizations
5
+ # are linked to their industry via the `works_in_industry` relationship.
6
+ #
7
+ # All elements are passive (deterministic extraction from filing headers).
8
+ name: "industries"
9
+ description: "SIC industry classifications linking SEC-registered organizations to their Standard Industrial Classification codes"
10
+
11
+ extraction:
12
+ flavors: closed
13
+ properties: closed
14
+ relationships: closed
15
+ attributes: closed
16
+
17
+ flavors:
18
+ - name: "industry"
19
+ description: "A Standard Industrial Classification (SIC) industry category assigned by the SEC"
20
+ display_name: "Industry"
21
+ mergeability: not_mergeable
22
+ strong_id_properties: ["sic_code"]
23
+ passive: true
24
+
25
+ - name: "organization"
26
+ description: "A particular business, institution, or organization such as a corporation, university, government agency, or non-profit"
27
+ display_name: "Organization"
28
+ mergeability: not_mergeable
29
+ strong_id_properties: ["company_cik"]
30
+ passive: true
31
+
32
+ properties:
33
+ - name: "sic_code"
34
+ type: string
35
+ description: "Four-digit Standard Industrial Classification code"
36
+ display_name: "SIC Code"
37
+ mergeability: not_mergeable
38
+ domain_flavors: ["industry", "organization"]
39
+ passive: true
40
+
41
+ - name: "sic_description"
42
+ type: string
43
+ description: "Human-readable SIC code description"
44
+ display_name: "SIC Description"
45
+ mergeability: not_mergeable
46
+ domain_flavors: ["industry", "organization"]
47
+ passive: true
48
+
49
+ - name: "company_cik"
50
+ type: string
51
+ description: "SEC Central Index Key, 10-digit zero-padded"
52
+ display_name: "CIK"
53
+ mergeability: not_mergeable
54
+ domain_flavors: ["organization"]
55
+ passive: true
56
+
57
+ relationships:
58
+ - name: "works_in_industry"
59
+ description: "Link from an organization to its SIC industry classification"
60
+ display_name: "Works In Industry"
61
+ mergeability: not_mergeable
62
+ domain_flavors: ["organization"]
63
+ target_flavors: ["industry"]
64
+ passive: true
@@ -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