@cyanheads/congressgov-mcp-server 0.3.18 → 0.3.19

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.
Files changed (30) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +5 -3
  4. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts +4 -0
  5. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts.map +1 -1
  6. package/dist/mcp-server/tools/definitions/bill-lookup.tool.js +11 -3
  7. package/dist/mcp-server/tools/definitions/bill-lookup.tool.js.map +1 -1
  8. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts.map +1 -1
  9. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js +24 -9
  10. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/enacted-laws.tool.js +1 -1
  12. package/dist/mcp-server/tools/definitions/enacted-laws.tool.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts +4 -0
  14. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts.map +1 -1
  15. package/dist/mcp-server/tools/definitions/roll-votes.tool.js +45 -1
  16. package/dist/mcp-server/tools/definitions/roll-votes.tool.js.map +1 -1
  17. package/dist/mcp-server/tools/definitions/senate-nominations.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js +20 -5
  19. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/format-helpers.d.ts +2 -2
  21. package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/format-helpers.js +437 -27
  23. package/dist/mcp-server/tools/format-helpers.js.map +1 -1
  24. package/dist/services/congress-api/congress-api-service.d.ts.map +1 -1
  25. package/dist/services/congress-api/congress-api-service.js +64 -13
  26. package/dist/services/congress-api/congress-api-service.js.map +1 -1
  27. package/dist/services/congress-api/types.d.ts +1 -0
  28. package/dist/services/congress-api/types.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/server.json +3 -3
@@ -14,16 +14,31 @@
14
14
  function tb(content) {
15
15
  return [{ type: 'text', text: content }];
16
16
  }
17
- function stripHtml(html) {
18
- return html
17
+ /**
18
+ * Strip HTML to plain text while preserving paragraph and line breaks. Upstream
19
+ * summary fields and other narrative bodies ship as HTML; we want the visible
20
+ * structure (paragraph boundaries) to survive into the rendered Markdown.
21
+ *
22
+ * Inline contexts that need single-line output should pass `{ inline: true }`.
23
+ */
24
+ function stripHtml(html, { inline = false } = {}) {
25
+ const text = html
26
+ .replace(/<\s*br\s*\/?\s*>/gi, '\n')
27
+ .replace(/<\s*\/p\s*>/gi, '\n\n')
19
28
  .replace(/<[^>]*>/g, '')
20
29
  .replace(/&nbsp;/g, ' ')
21
30
  .replace(/&#039;/g, "'")
22
31
  .replace(/&amp;/g, '&')
23
32
  .replace(/&lt;/g, '<')
24
33
  .replace(/&gt;/g, '>')
25
- .replace(/&quot;/g, '"')
26
- .replace(/\s+/g, ' ')
34
+ .replace(/&quot;/g, '"');
35
+ if (inline)
36
+ return text.replace(/\s+/g, ' ').trim();
37
+ return text
38
+ .replace(/[ \t]+/g, ' ')
39
+ .replace(/\n[ \t]+/g, '\n')
40
+ .replace(/[ \t]+\n/g, '\n')
41
+ .replace(/\n{3,}/g, '\n\n')
27
42
  .trim();
28
43
  }
29
44
  /**
@@ -54,7 +69,7 @@ function htmlToMarkdown(html) {
54
69
  .replace(/\n{3,}/g, '\n\n')
55
70
  .trim();
56
71
  }
57
- /** Safe deep access with HTML stripping. Handles string and number values. */
72
+ /** Safe deep access for compact field display collapses whitespace to a single line. */
58
73
  function s(obj, ...path) {
59
74
  let cur = obj;
60
75
  for (const key of path) {
@@ -63,7 +78,7 @@ function s(obj, ...path) {
63
78
  cur = cur[key];
64
79
  }
65
80
  if (typeof cur === 'string')
66
- return stripHtml(cur);
81
+ return stripHtml(cur, { inline: true });
67
82
  if (typeof cur === 'number')
68
83
  return String(cur);
69
84
  return;
@@ -85,11 +100,11 @@ function pagHeader(result) {
85
100
  return `**${count} result${count !== 1 ? 's' : ''}**${next != null ? ` | next offset: ${next}` : ''}`;
86
101
  }
87
102
  /** Render a paginated list with header and per-item rendering. */
88
- function renderList(result, renderItem) {
103
+ function renderList(result, renderItem, emptyHint) {
89
104
  const items = (result.data ?? []);
90
105
  const header = pagHeader(result);
91
106
  if (items.length === 0) {
92
- return `${header}\n\nNo results matched the query. Try broadening the filters — widen the date range, drop the type/chamber/state constraint, or remove the date window entirely.`;
107
+ return emptyHint ? `${header}\n\n${emptyHint}` : header;
93
108
  }
94
109
  const renderer = renderItem ?? renderGenericItem;
95
110
  const rendered = items.map((item, i) => typeof item === 'object' && item !== null
@@ -118,13 +133,13 @@ function renderDetail(obj) {
118
133
  if (val == null || val === '')
119
134
  continue;
120
135
  if (typeof val === 'string') {
121
- const cleaned = stripHtml(val);
122
- if (cleaned.length > 300) {
136
+ const inline = stripHtml(val, { inline: true });
137
+ if (inline.length > 300) {
123
138
  lines.push(`**${key}:**`);
124
- lines.push(cleaned);
139
+ lines.push(stripHtml(val));
125
140
  }
126
141
  else {
127
- lines.push(`**${key}:** ${cleaned}`);
142
+ lines.push(`**${key}:** ${inline}`);
128
143
  }
129
144
  }
130
145
  else if (typeof val === 'number' || typeof val === 'boolean') {
@@ -172,7 +187,7 @@ function renderDetail(obj) {
172
187
  if (v2 == null || v2 === '')
173
188
  continue;
174
189
  if (typeof v2 === 'string')
175
- lines.push(` **${k2}:** ${stripHtml(v2)}`);
190
+ lines.push(` **${k2}:** ${stripHtml(v2, { inline: true })}`);
176
191
  else if (typeof v2 === 'number' || typeof v2 === 'boolean')
177
192
  lines.push(` **${k2}:** ${v2}`);
178
193
  else if (typeof v2 === 'object' && v2)
@@ -203,13 +218,13 @@ function renderGenericItem(item, index) {
203
218
  if (HEADING_FIELDS.has(key))
204
219
  continue;
205
220
  if (typeof val === 'string') {
206
- const cleaned = stripHtml(val);
207
- if (cleaned.length > 300) {
221
+ const inline = stripHtml(val, { inline: true });
222
+ if (inline.length > 300) {
208
223
  lines.push(`**${key}:**`);
209
- lines.push(cleaned);
224
+ lines.push(stripHtml(val));
210
225
  }
211
226
  else {
212
- lines.push(`**${key}:** ${cleaned}`);
227
+ lines.push(`**${key}:** ${inline}`);
213
228
  }
214
229
  }
215
230
  else if (typeof val === 'number' || typeof val === 'boolean') {
@@ -262,7 +277,7 @@ function renderInline(obj) {
262
277
  if (val == null || val === '')
263
278
  continue;
264
279
  if (typeof val === 'string') {
265
- const cleaned = stripHtml(val);
280
+ const cleaned = stripHtml(val, { inline: true });
266
281
  const preview = cleaned.length > 120 ? `${cleaned.slice(0, 117)}...` : cleaned;
267
282
  parts.push(`${key}: ${preview}`);
268
283
  }
@@ -390,7 +405,8 @@ function renderCrsReportItem(item, i) {
390
405
  ]);
391
406
  if (meta)
392
407
  lines.push(meta);
393
- lines.push(summary || '_Summary not available._');
408
+ if (summary)
409
+ lines.push(summary);
394
410
  if (url)
395
411
  lines.push(`**URL:** ${url}`);
396
412
  return lines.join('\n');
@@ -522,6 +538,348 @@ function renderCommitteeReportTextItem(item, i) {
522
538
  }
523
539
  return lines.join('\n');
524
540
  }
541
+ /** Member-sponsored amendments — `type`/`title` are null upstream; identify by `amendmentNumber`. */
542
+ function renderAmendmentItem(item, i) {
543
+ const number = s(item, 'amendmentNumber');
544
+ const url = s(item, 'url') ?? '';
545
+ /** URL path carries the chamber prefix (samdt / hamdt) we need for a readable type label. */
546
+ const amdMatch = url.match(/\/amendment\/(\d+)\/(samdt|hamdt|suamdt|huamdt)\//i);
547
+ const typeCode = amdMatch?.[2]?.toLowerCase();
548
+ const chamber = typeCode === 'samdt' || typeCode === 'suamdt'
549
+ ? 'Senate Amendment'
550
+ : typeCode === 'hamdt' || typeCode === 'huamdt'
551
+ ? 'House Amendment'
552
+ : 'Amendment';
553
+ const heading = number ? `${chamber} ${number}` : 'Amendment';
554
+ const lines = [`### ${i + 1}. ${heading}`];
555
+ const meta = join([
556
+ f('Congress', s(item, 'congress')),
557
+ f('Introduced', s(item, 'introducedDate')),
558
+ ]);
559
+ if (meta)
560
+ lines.push(meta);
561
+ const actionDate = s(item, 'latestAction', 'actionDate');
562
+ const actionText = s(item, 'latestAction', 'text');
563
+ if (actionDate || actionText)
564
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
565
+ if (url)
566
+ lines.push(`**URL:** ${url}`);
567
+ return lines.join('\n');
568
+ }
569
+ /** Bill text versions — heading from `type` (e.g. "Enrolled Bill"), formats[] as labeled URLs. */
570
+ function renderBillTextItem(item, i) {
571
+ const type = s(item, 'type') ?? 'Bill Text';
572
+ const date = s(item, 'date');
573
+ const lines = [`### ${i + 1}. ${type}`];
574
+ if (date)
575
+ lines.push(`**Date:** ${date}`);
576
+ const formats = item.formats;
577
+ if (Array.isArray(formats)) {
578
+ for (const fmt of formats) {
579
+ const fType = s(fmt, 'type');
580
+ const fUrl = s(fmt, 'url');
581
+ if (fType && fUrl)
582
+ lines.push(`**${fType}:** ${fUrl}`);
583
+ }
584
+ }
585
+ return lines.join('\n');
586
+ }
587
+ /** Nomination type wrapper: `{isCivilian: true}` / `{isMilitary: true}` → readable label. */
588
+ function nominationTypeLabel(raw) {
589
+ if (!raw || typeof raw !== 'object')
590
+ return;
591
+ const t = raw;
592
+ if (t.isCivilian === true)
593
+ return 'Civilian';
594
+ if (t.isMilitary === true)
595
+ return 'Military';
596
+ return;
597
+ }
598
+ function nominationHeading(item) {
599
+ const citation = s(item, 'citation');
600
+ if (citation)
601
+ return citation;
602
+ const number = s(item, 'number');
603
+ const partNumber = s(item, 'partNumber');
604
+ if (number && partNumber && partNumber !== '00')
605
+ return `PN${number}-${Number(partNumber)}`;
606
+ if (number)
607
+ return `PN${number}`;
608
+ return 'Nomination';
609
+ }
610
+ function renderNominationListItem(item, i) {
611
+ const heading = nominationHeading(item);
612
+ const type = nominationTypeLabel(item.nominationType);
613
+ const lines = [`### ${i + 1}. ${heading}`];
614
+ const description = s(item, 'description');
615
+ if (description)
616
+ lines.push(description);
617
+ const meta = join([
618
+ f('Congress', s(item, 'congress')),
619
+ f('Type', type),
620
+ f('Received', s(item, 'receivedDate')),
621
+ f('Authority Date', s(item, 'authorityDate')),
622
+ f('Updated', s(item, 'updateDate')),
623
+ ]);
624
+ if (meta)
625
+ lines.push(meta);
626
+ const actionDate = s(item, 'latestAction', 'actionDate');
627
+ const actionText = s(item, 'latestAction', 'text');
628
+ if (actionDate || actionText)
629
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
630
+ const url = s(item, 'url');
631
+ if (url)
632
+ lines.push(`**URL:** ${url}`);
633
+ return lines.join('\n');
634
+ }
635
+ function renderNominationDetail(item) {
636
+ const heading = nominationHeading(item);
637
+ const type = nominationTypeLabel(item.nominationType);
638
+ const lines = [`# ${heading}`];
639
+ const description = s(item, 'description');
640
+ if (description)
641
+ lines.push(description);
642
+ const meta = join([
643
+ f('Congress', s(item, 'congress')),
644
+ f('Type', type),
645
+ f('Part Number', s(item, 'partNumber')),
646
+ f('Received', s(item, 'receivedDate')),
647
+ f('Authority Date', s(item, 'authorityDate')),
648
+ f('Updated', s(item, 'updateDate')),
649
+ ]);
650
+ if (meta)
651
+ lines.push(meta);
652
+ const actionDate = s(item, 'latestAction', 'actionDate');
653
+ const actionText = s(item, 'latestAction', 'text');
654
+ if (actionDate || actionText)
655
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
656
+ const subResources = [];
657
+ for (const key of ['actions', 'committees', 'hearings']) {
658
+ const sub = item[key];
659
+ if (sub && typeof sub.count === 'number' && sub.count > 0)
660
+ subResources.push(`${sub.count} ${key}`);
661
+ }
662
+ if (subResources.length)
663
+ lines.push(`**Available:** ${subResources.join(', ')}`);
664
+ const nominees = item.nominees;
665
+ if (Array.isArray(nominees) && nominees.length > 0) {
666
+ lines.push(`\n**Nominees (${nominees.length}):**`);
667
+ for (const n of nominees.slice(0, 20)) {
668
+ const ord = s(n, 'ordinal');
669
+ const count = s(n, 'nomineeCount');
670
+ const org = s(n, 'organization');
671
+ const title = s(n, 'positionTitle');
672
+ const parts = [
673
+ ord ? `Ord ${ord}` : undefined,
674
+ count ? `${count} nominee(s)` : undefined,
675
+ org,
676
+ title,
677
+ ].filter(Boolean);
678
+ lines.push(`- ${parts.join(' — ')}`);
679
+ }
680
+ if (nominees.length > 20)
681
+ lines.push(`- _...${nominees.length - 20} more_`);
682
+ }
683
+ else if (s(item, 'partNumber') === '00') {
684
+ /** Parent nominations (partNumber=00) have no nominees array. Sub-resources
685
+ * also return 0 results — they live on the partitioned children (PN851-1, PN851-2, …). */
686
+ lines.push('\n_This is a parent nomination. Individual nominees and confirmation actions live on partitioned children (e.g. `PN851-1`, `PN851-2`). Use the partitioned form for `actions`, `committees`, `hearings`, or `nominees`._');
687
+ }
688
+ const url = s(item, 'url');
689
+ if (url)
690
+ lines.push(`\n**URL:** ${url}`);
691
+ return lines.join('\n');
692
+ }
693
+ /** Roll call vote detail — question, result, date, party totals. */
694
+ function renderRollVoteDetail(item) {
695
+ const roll = s(item, 'rollCallNumber');
696
+ const congress = s(item, 'congress');
697
+ const session = s(item, 'sessionNumber');
698
+ const result = s(item, 'result');
699
+ const question = s(item, 'voteQuestion');
700
+ const voteType = s(item, 'voteType');
701
+ const startDate = s(item, 'startDate');
702
+ const updated = s(item, 'updateDate');
703
+ const identifier = s(item, 'identifier');
704
+ const sourceUrl = s(item, 'sourceDataURL');
705
+ const headingLeft = roll ? `Roll ${roll}` : 'Roll call';
706
+ const heading = result ? `${headingLeft} — ${result}` : headingLeft;
707
+ const lines = [`# ${heading}`];
708
+ if (question)
709
+ lines.push(`**Question:** ${question}`);
710
+ const meta = join([
711
+ f('Congress', congress),
712
+ f('Session', session),
713
+ f('Type', voteType),
714
+ f('Date', startDate),
715
+ f('Updated', updated),
716
+ identifier && identifier !== roll ? f('ID', identifier) : undefined,
717
+ ]);
718
+ if (meta)
719
+ lines.push(meta);
720
+ const totals = item.votePartyTotal;
721
+ if (Array.isArray(totals) && totals.length > 0) {
722
+ lines.push('\n**Party Totals:**');
723
+ for (const t of totals) {
724
+ const party = s(t, 'party', 'name') ?? s(t, 'voteParty') ?? '?';
725
+ const yea = s(t, 'yeaTotal') ?? '0';
726
+ const nay = s(t, 'nayTotal') ?? '0';
727
+ const present = s(t, 'presentTotal') ?? '0';
728
+ const notVoting = s(t, 'notVotingTotal') ?? '0';
729
+ lines.push(`- **${party}:** Yea ${yea}, Nay ${nay}, Present ${present}, Not Voting ${notVoting}`);
730
+ }
731
+ }
732
+ const results = item.results;
733
+ if (Array.isArray(results) && results.length > 0) {
734
+ lines.push(`\n**Member Votes:** ${results.length} on this page`);
735
+ for (const r of results.slice(0, 20)) {
736
+ const name = s(r, 'firstName') && s(r, 'lastName')
737
+ ? `${s(r, 'firstName')} ${s(r, 'lastName')}`
738
+ : (s(r, 'bioguideId') ?? '?');
739
+ const vote = s(r, 'voteCast');
740
+ const party = s(r, 'voteParty');
741
+ const state = s(r, 'voteState');
742
+ const parts = [
743
+ name,
744
+ party && state ? `(${party}-${state})` : undefined,
745
+ vote ? `→ ${vote}` : undefined,
746
+ ].filter(Boolean);
747
+ lines.push(`- ${parts.join(' ')}`);
748
+ }
749
+ if (results.length > 20)
750
+ lines.push(`- _...${results.length - 20} more_`);
751
+ }
752
+ if (sourceUrl)
753
+ lines.push(`\n**Source Data URL:** ${sourceUrl}`);
754
+ return lines.join('\n');
755
+ }
756
+ /** Member detail. */
757
+ function renderMemberDetail(item) {
758
+ const name = s(item, 'directOrderName') ??
759
+ s(item, 'invertedOrderName') ??
760
+ s(item, 'bioguideId') ??
761
+ 'Unknown';
762
+ const lines = [`# ${name}`];
763
+ const meta = join([
764
+ f('ID', s(item, 'bioguideId')),
765
+ f('Party', s(item, 'partyName') ?? s(item, 'currentParty')),
766
+ f('State', s(item, 'state')),
767
+ item.district != null ? f('District', s(item, 'district')) : undefined,
768
+ f('Currently Serving', typeof item.currentMember === 'boolean' ? String(item.currentMember) : undefined),
769
+ f('Birth Year', s(item, 'birthYear')),
770
+ f('Updated', s(item, 'updateDate')),
771
+ ]);
772
+ if (meta)
773
+ lines.push(meta);
774
+ const honorific = s(item, 'honorificName');
775
+ if (honorific)
776
+ lines.push(`**Honorific:** ${honorific}`);
777
+ /** terms may be a direct array or nested as `{item: [...]}`. */
778
+ const rawTerms = item.terms;
779
+ const termsArr = Array.isArray(rawTerms)
780
+ ? rawTerms
781
+ : rawTerms &&
782
+ typeof rawTerms === 'object' &&
783
+ Array.isArray(rawTerms.item)
784
+ ? rawTerms.item
785
+ : undefined;
786
+ if (termsArr && termsArr.length > 0) {
787
+ lines.push(`\n**Terms (${termsArr.length}):**`);
788
+ const recent = termsArr.slice(-5);
789
+ for (const term of recent) {
790
+ const chamber = s(term, 'chamber');
791
+ const start = s(term, 'startYear');
792
+ const end = s(term, 'endYear');
793
+ const party = s(term, 'partyName');
794
+ const stateName = s(term, 'stateName');
795
+ const range = start && end ? `${start}–${end}` : start;
796
+ const parts = [chamber, range, party, stateName].filter(Boolean);
797
+ lines.push(`- ${parts.join(', ')}`);
798
+ }
799
+ if (termsArr.length > 5)
800
+ lines.push(`- _...${termsArr.length - 5} earlier_`);
801
+ }
802
+ const partyHistory = item.partyHistory;
803
+ if (Array.isArray(partyHistory) && partyHistory.length > 0) {
804
+ lines.push(`\n**Party History:**`);
805
+ for (const p of partyHistory) {
806
+ const partyName = s(p, 'partyName');
807
+ const start = s(p, 'startYear');
808
+ const end = s(p, 'endYear');
809
+ const range = start && end ? `${start}–${end}` : start;
810
+ const parts = [partyName, range && `(${range})`].filter(Boolean);
811
+ lines.push(`- ${parts.join(' ')}`);
812
+ }
813
+ }
814
+ const leadership = item.leadership;
815
+ if (Array.isArray(leadership) && leadership.length > 0) {
816
+ lines.push(`\n**Leadership Roles (${leadership.length}):**`);
817
+ for (const l of leadership.slice(0, 10)) {
818
+ const type = s(l, 'type');
819
+ const congress = s(l, 'congress');
820
+ lines.push(`- ${[type, congress ? `Congress ${congress}` : undefined].filter(Boolean).join(' — ')}`);
821
+ }
822
+ if (leadership.length > 10)
823
+ lines.push(`- _...${leadership.length - 10} more_`);
824
+ }
825
+ const subResources = [];
826
+ for (const key of ['sponsoredLegislation', 'cosponsoredLegislation']) {
827
+ const sub = item[key];
828
+ if (sub && typeof sub.count === 'number' && sub.count > 0) {
829
+ const label = key === 'sponsoredLegislation' ? 'sponsored' : 'cosponsored';
830
+ subResources.push(`${sub.count} ${label}`);
831
+ }
832
+ }
833
+ if (subResources.length)
834
+ lines.push(`\n**Legislation:** ${subResources.join(', ')}`);
835
+ const url = s(item, 'url');
836
+ if (url)
837
+ lines.push(`\n**URL:** ${url}`);
838
+ return lines.join('\n');
839
+ }
840
+ /** Committee list item — name + key fields. */
841
+ function renderCommitteeListItem(item, i) {
842
+ const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
843
+ const lines = [`### ${i + 1}. ${name}`];
844
+ const meta = join([
845
+ f('Code', s(item, 'systemCode')),
846
+ f('Chamber', s(item, 'chamber')),
847
+ f('Type', s(item, 'committeeTypeCode')),
848
+ f('Updated', s(item, 'updateDate')),
849
+ ]);
850
+ if (meta)
851
+ lines.push(meta);
852
+ const url = s(item, 'url');
853
+ if (url)
854
+ lines.push(`**URL:** ${url}`);
855
+ return lines.join('\n');
856
+ }
857
+ /** Committee report list item — citation-first; upstream omits title and bill ref. */
858
+ function renderCommitteeReportListItem(item, i) {
859
+ const citation = s(item, 'citation');
860
+ const type = s(item, 'type');
861
+ const number = s(item, 'number');
862
+ const part = s(item, 'part');
863
+ const congress = s(item, 'congress');
864
+ const chamber = s(item, 'chamber');
865
+ const updated = s(item, 'updateDate');
866
+ const url = s(item, 'url');
867
+ const heading = citation ?? (type && number ? `${type} ${congress ?? ''}-${number}` : 'Committee Report');
868
+ const lines = [`### ${i + 1}. ${heading}`];
869
+ const meta = join([
870
+ f('Congress', congress),
871
+ f('Chamber', chamber),
872
+ f('Type', type),
873
+ f('Number', number),
874
+ part && part !== '1' ? f('Part', part) : undefined,
875
+ f('Updated', updated),
876
+ ]);
877
+ if (meta)
878
+ lines.push(meta);
879
+ if (url)
880
+ lines.push(`**URL:** ${url}`);
881
+ return lines.join('\n');
882
+ }
525
883
  // ── Per-Tool Format Exports ─────────────────────────────────────────
526
884
  function makeFormatter(detailKeys, itemRenderer) {
527
885
  return (result) => {
@@ -546,10 +904,35 @@ export function formatBills(result) {
546
904
  return tb(renderDetail(result.bill));
547
905
  return tb(renderDetail(result));
548
906
  }
907
+ /**
908
+ * Bill sub-resource summary item — known shape (no nested `bill.*`, since the
909
+ * caller already has the bill). Reuses `htmlToMarkdown` so `<p>` / `<strong>`
910
+ * survive into the rendered Markdown.
911
+ */
912
+ function renderBillSubresourceSummaryItem(item, i) {
913
+ const version = s(item, 'actionDesc') ?? s(item, 'versionCode') ?? 'Summary';
914
+ const actionDate = s(item, 'actionDate');
915
+ const updated = s(item, 'updateDate');
916
+ const lines = [`### ${i + 1}. ${version}`];
917
+ const meta = join([f('Action Date', actionDate), f('Summary Updated', updated)]);
918
+ if (meta)
919
+ lines.push(meta);
920
+ const text = typeof item.text === 'string' ? htmlToMarkdown(item.text) : '';
921
+ if (text)
922
+ lines.push('', text);
923
+ return lines.join('\n');
924
+ }
549
925
  function pickBillListRenderer(first) {
550
926
  if ('title' in first && 'number' in first)
551
927
  return renderBillItem;
552
- // Actions always ship a `text` body; most also carry actionDate/actionCode/sourceSystem.
928
+ /** Bill text versions: `type` + `formats[]`, no `actionDate`. */
929
+ if ('type' in first && 'formats' in first)
930
+ return renderBillTextItem;
931
+ /** Bill sub-resource summaries: `actionDesc`/`versionCode` + `text`, no `actionCode`/`sourceSystem`. */
932
+ if ('text' in first && ('actionDesc' in first || 'versionCode' in first)) {
933
+ return renderBillSubresourceSummaryItem;
934
+ }
935
+ /** Actions always ship a `text` body; most also carry actionDate/actionCode/sourceSystem. */
553
936
  if ('text' in first &&
554
937
  ('actionDate' in first || 'actionCode' in first || 'sourceSystem' in first))
555
938
  return renderBillActionItem;
@@ -564,12 +947,18 @@ export function formatMembers(result) {
564
947
  const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
565
948
  if (firstRecord && 'bioguideId' in firstRecord)
566
949
  return tb(renderList(result, renderMemberItem));
567
- if (firstRecord && 'number' in firstRecord && 'title' in firstRecord)
568
- return tb(renderList(result, renderBillItem));
950
+ /** Sponsored/cosponsored may mix bills (type+title) and amendments (amendmentNumber, null type/title).
951
+ * Dispatch per-row so amendments don't render as 'Untitled'. */
952
+ if (firstRecord && ('number' in firstRecord || 'amendmentNumber' in firstRecord)) {
953
+ const dispatch = (item, i) => 'amendmentNumber' in item && item.amendmentNumber != null
954
+ ? renderAmendmentItem(item, i)
955
+ : renderBillItem(item, i);
956
+ return tb(renderList(result, dispatch));
957
+ }
569
958
  return tb(renderList(result));
570
959
  }
571
960
  if (result.member != null)
572
- return tb(renderDetail(result.member));
961
+ return tb(renderMemberDetail(result.member));
573
962
  return tb(renderDetail(result));
574
963
  }
575
964
  /** Pull the committee's display name from nested history when the top-level `name` is missing. */
@@ -584,8 +973,15 @@ function extractCommitteeName(committee) {
584
973
  }
585
974
  /** Committee browse, detail, and sub-resources (bills, reports, nominations). */
586
975
  export function formatCommittees(result) {
587
- if (Array.isArray(result.data))
976
+ if (Array.isArray(result.data)) {
977
+ const first = result.data[0];
978
+ const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
979
+ /** Committee list rows have `systemCode` + `name`. Sub-resource rows
980
+ * (bills/reports/nominations) keep their generic / specialized renderers. */
981
+ if (firstRecord && 'systemCode' in firstRecord && 'name' in firstRecord)
982
+ return tb(renderList(result, renderCommitteeListItem));
588
983
  return tb(renderList(result));
984
+ }
589
985
  if (result.committee != null) {
590
986
  const committee = result.committee;
591
987
  const name = extractCommitteeName(committee);
@@ -597,7 +993,7 @@ export function formatCommittees(result) {
597
993
  /** Committee reports — list, detail, and text. */
598
994
  export function formatCommitteeReports(result) {
599
995
  if (Array.isArray(result.data))
600
- return tb(renderList(result));
996
+ return tb(renderList(result, renderCommitteeReportListItem));
601
997
  if (Array.isArray(result.text)) {
602
998
  const textResult = { data: result.text, pagination: { count: result.text.length } };
603
999
  return tb(renderList(textResult, renderCommitteeReportTextItem));
@@ -632,7 +1028,21 @@ export function formatDailyRecord(result) {
632
1028
  /** Enacted public and private laws. */
633
1029
  export const formatLaws = makeFormatter(['law'], renderBillItem);
634
1030
  /** House roll call votes and member voting positions. */
635
- export const formatVotes = makeFormatter(['vote'], renderRollVoteItem);
1031
+ export function formatVotes(result) {
1032
+ if (Array.isArray(result.data))
1033
+ return tb(renderList(result, renderRollVoteItem));
1034
+ if (result.vote != null)
1035
+ return tb(renderRollVoteDetail(result.vote));
1036
+ return tb(renderDetail(result));
1037
+ }
636
1038
  /** Presidential nominations and Senate confirmation pipeline. */
637
- export const formatNominations = makeFormatter(['nomination']);
1039
+ export function formatNominations(result) {
1040
+ if (Array.isArray(result.data)) {
1041
+ const hint = typeof result.emptyHint === 'string' ? result.emptyHint : undefined;
1042
+ return tb(renderList(result, renderNominationListItem, hint));
1043
+ }
1044
+ if (result.nomination != null)
1045
+ return tb(renderNominationDetail(result.nomination));
1046
+ return tb(renderDetail(result));
1047
+ }
638
1048
  //# sourceMappingURL=format-helpers.js.map