search_issuers_by_state
Retrieve all municipal issuers for a given state, with optional name filtering, to obtain issuer IDs for detailed bond or profile queries.
Instructions
List municipal issuers for a state (all 9k+ rows, not just page 1) and filter by name. Feed the issuer_id into get_issuer_outstanding_bonds or get_issuer_profile to drill down. IMPORTANT: the contains filter auto-expands your query into every EMMA abbreviation variant (hospital→HOSP, authority→AUTH, children→CHLDN, senior→SR, community→CMNTY, etc.) — always search in natural English, never in the telegraphic form. For hospital/senior-living/charter-school obligors, search the CONDUIT, not the obligor: e.g. CHLA bonds are filed under CALIFORNIA PUB FIN AUTH HEALTH CARE FACS REV — found by searching contains="health care facilities". See the emma://rules/EMMA_ABBREVIATIONS.md resource for the full variant map and conduit cheat sheet.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| state | Yes | 2-letter state code | |
| contains | No | Case-insensitive name filter | |
| limit | No |
Implementation Reference
- server.py:1111-1154 (handler)The main handler for the 'search_issuers_by_state' tool. Uses Playwright to navigate to EMMA's IssuerHomePage/State page, waits for the DataTable to fully load (including all client-side rows), extracts all issuer records (issuer_id, name, type, url) via jQuery DataTable API in the browser, then filters by the optional 'contains' parameter using the abbreviation-aware _query_patterns helper. Returns issuers up to the 'limit'.
if name == "search_issuers_by_state": state = args["state"].strip().upper() async with new_page() as page: await page.goto(f"{EMMA_BASE}/IssuerHomePage/State?state={state}", wait_until=NAV_WAIT, timeout=NAV_TIMEOUT_MS) # DataTable loads 9k+ rows client-side; wait for the table instance await page.wait_for_function( "() => { const j=window.jQuery||window.$; " "return !!(j && j('#lvIssuers').DataTable && " "j('#lvIssuers').DataTable().rows().count() > 0); }", timeout=20000, ) rows = await page.evaluate( """() => { const jq = window.jQuery || window.$; const dt = jq('#lvIssuers').DataTable(); return dt.rows().data().toArray().map(r => ({ issuer_id: r.id, name: r.nm || '', issuer_type: r.itp || '', url: 'https://emma.msrb.org/IssuerHomePage/Issuer?id=' + r.id + '&type=' + (r.tp || 'M'), })); }""" ) total_in_state = len(rows) contains = (args.get("contains") or "").strip() expanded_terms: list[str] = [] if contains: patterns = _query_patterns(contains) expanded_terms = [p.pattern for p in patterns] rows = [i for i in rows if _match_query(i["name"], patterns)] limit = int(args.get("limit", 50)) return { "state": state, "total_in_state": total_in_state, "count": len(rows), "query_expansion_notes": ( "EMMA uses legacy abbreviations (HOSP, AUTH, CHLDN, CMNTY, " "FING, REV). Query tokens are expanded to known variants." if expanded_terms else None ), "expanded_regex": expanded_terms or None, "issuers": rows[:limit], } - server.py:506-534 (registration)Tool registration with name 'search_issuers_by_state', input schema (state required, contains optional, limit default 50), and description in the list_tools() function.
name="search_issuers_by_state", description=( "List municipal issuers for a state (all 9k+ rows, not just " "page 1) and filter by name. Feed the issuer_id into " "get_issuer_outstanding_bonds or get_issuer_profile to drill " "down. IMPORTANT: the `contains` filter auto-expands your " "query into every EMMA abbreviation variant (hospital→HOSP, " "authority→AUTH, children→CHLDN, senior→SR, community→CMNTY, " "etc.) — always search in natural English, never in the " "telegraphic form. For hospital/senior-living/charter-school " "obligors, search the CONDUIT, not the obligor: e.g. CHLA " "bonds are filed under CALIFORNIA PUB FIN AUTH HEALTH CARE " "FACS REV — found by searching `contains=\"health care " "facilities\"`. See the emma://rules/EMMA_ABBREVIATIONS.md " "resource for the full variant map and conduit cheat sheet." ), inputSchema={ "type": "object", "properties": { "state": {"type": "string", "description": "2-letter state code"}, "contains": { "type": "string", "description": "Case-insensitive name filter", }, "limit": {"type": "integer", "default": 50}, }, "required": ["state"], }, ), - server.py:253-278 (helper)Helper functions _query_patterns and _match_query used by search_issuers_by_state to expand user queries into EMMA abbreviation variants and filter issuer names with AND semantics.
def _query_patterns(query: str) -> list[re.Pattern[str]]: """Tokenize a query and return one regex per token, each matching any known variant of that word. All patterns must match (AND across tokens).""" if not query: return [] # Collapse whitespace, strip punctuation except hyphens clean = re.sub(r"[^\w\s-]+", " ", query.lower()).strip() tokens = [t for t in clean.split() if t] patterns: list[re.Pattern[str]] = [] for tok in tokens: variants: set[str] = {tok} if tok in MUNI_ABBREV: variants.update(MUNI_ABBREV[tok]) if tok in _ABBREV_REVERSE: for long_form in _ABBREV_REVERSE[tok]: variants.update(MUNI_ABBREV.get(long_form, [])) # Escape each variant and combine. Word-boundary on each side so "ca" # doesn't match inside "cares" but does match "CA HOSP" or " CA ". parts = sorted(variants, key=len, reverse=True) pat = r"(?<![A-Za-z])(?:" + "|".join(re.escape(v) for v in parts) + r")(?![A-Za-z])" patterns.append(re.compile(pat, re.IGNORECASE)) return patterns def _match_query(name: str, patterns: list[re.Pattern[str]]) -> bool: return all(p.search(name) for p in patterns)