@pipeworx/mcp-openstates 0.1.3 → 0.1.5

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": "@pipeworx/mcp-openstates",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenStates MCP — bills, legislators, votes in all 50 US states",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,7 +26,7 @@
26
26
  "@cloudflare/workers-types": "^4.20260405.1"
27
27
  },
28
28
  "pipeworx": {
29
- "sourceHash": "v1-cb3703013cf69fc6e2fc70bca57a6857b7ead99de0e71beaa6a32b1312e11733",
30
- "sourceCommit": "c7cc6672266b20d1b5feb0c4c2d631e50ad8ba17"
29
+ "sourceHash": "v1-732159c87c7a46974cc5b92992d343278cc7c9e5f77023cc3f442dfd8969be69",
30
+ "sourceCommit": "ca77f046c3ddc52178f0c89d5682d4625c655201"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.pipeworx-io/openstates",
4
4
  "title": "Openstates",
5
5
  "description": "OpenStates MCP — bills, legislators, votes in all 50 US states",
6
- "version": "0.1.3",
6
+ "version": "0.1.5",
7
7
  "websiteUrl": "https://pipeworx.io/packs/openstates",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-openstates",
package/src/index.ts CHANGED
@@ -909,13 +909,19 @@ async function searchBills(apiKey: string, args: Record<string, unknown>) {
909
909
  bills: (data.results ?? []).map((b) => normalizeBill(b, false)),
910
910
  });
911
911
 
912
+ // Session ids are per-state (fleet #2465), so a zero under a session filter
913
+ // is as likely a wrong-shaped id as a genuine no-match. A year-shaped value
914
+ // is the common wrong shape: fetch the state's session list ALONGSIDE the
915
+ // search rather than after it, so the retry costs one extra round trip, not
916
+ // two — ask_pipeworx gives a leg ~13s and this upstream is slow.
917
+ const resolutionP: Promise<Settled> | null =
918
+ session && yearRangeOf(session) ? resolveSession(apiKey, jurisdiction, session).then(ok, fail) : null;
912
919
  const data = await osFetch<BillsPage>(apiKey, '/bills', params);
913
920
  if (!session || (data.results?.length ?? 0) > 0) return shape(data);
914
921
 
915
- // Zero rows WITH a session filter: session ids are per-state (fleet #2465),
916
- // so a zero here is as likely a wrong-shaped id as a genuine no-match. Check
917
- // it against the state's own session list before reporting an empty result.
918
- const resolution = await resolveSession(apiKey, jurisdiction, session);
922
+ const settled = resolutionP ? await resolutionP : await resolveSession(apiKey, jurisdiction, session).then(ok, fail);
923
+ if ('error' in settled) throw settled.error;
924
+ const resolution = settled.value;
919
925
  if (resolution.exact) return shape(data, { session_checked: resolution.exact });
920
926
 
921
927
  let last = data;
@@ -934,6 +940,11 @@ async function searchBills(apiKey: string, args: Record<string, unknown>) {
934
940
  });
935
941
  }
936
942
 
943
+ type Resolution = { exact?: string; candidates: OsSession[] };
944
+ type Settled = { value: Resolution } | { error: unknown };
945
+ const ok = (value: Resolution): Settled => ({ value });
946
+ const fail = (error: unknown): Settled => ({ error });
947
+
937
948
  function sessionNote(requested: string, used: string, name: string | undefined, candidates: OsSession[]) {
938
949
  return {
939
950
  requested,
@@ -997,8 +1008,8 @@ export function resolveSessionCandidates(requested: string, sessions: OsSession[
997
1008
  return hits
998
1009
  .map((s, i) => ({ s, i }))
999
1010
  .sort((a, b) => {
1000
- const pa = a.s.classification === 'primary' ? 0 : 1;
1001
- const pb = b.s.classification === 'primary' ? 0 : 1;
1011
+ const pa = isSpecialSession(a.s) ? 1 : 0;
1012
+ const pb = isSpecialSession(b.s) ? 1 : 0;
1002
1013
  if (pa !== pb) return pa - pb;
1003
1014
  const da = a.s.start_date ?? '';
1004
1015
  const db = b.s.start_date ?? '';
@@ -1009,6 +1020,15 @@ export function resolveSessionCandidates(requested: string, sessions: OsSession[
1009
1020
  .slice(0, 4);
1010
1021
  }
1011
1022
 
1023
+ // Otherwise the newest special session outranks the regular one for the same
1024
+ // year ("2025" in Texas picked "892" over "89R").
1025
+ // Live Texas marks its called sessions `primary` too, so the name is the
1026
+ // signal that actually separates them; `classification` only adds to it.
1027
+ function isSpecialSession(s: OsSession): boolean {
1028
+ if (s.classification === 'special') return true;
1029
+ return /\b(special|called|extraordinary|extra)\b/i.test(s.name ?? '');
1030
+ }
1031
+
1012
1032
  const TERRITORIES = new Set(['pr', 'gu', 'vi', 'as', 'mp']);
1013
1033
 
1014
1034
  function jurisdictionPathId(jurisdiction: string): string {
@@ -1021,6 +1041,25 @@ function jurisdictionPathId(jurisdiction: string): string {
1021
1041
  return j.startsWith('ocd-jurisdiction/') ? j : encodeURIComponent(j);
1022
1042
  }
1023
1043
 
1044
+ // Session lists change a few times a year; the upstream free tier is small and
1045
+ // per-minute throttled, so keep them per isolate for an hour rather than
1046
+ // spending a request on every zero-row search.
1047
+ const SESSION_TTL_MS = 60 * 60 * 1000;
1048
+ const sessionCache = new Map<string, { at: number; data: { name?: string; legislative_sessions?: OsSession[] } }>();
1049
+
1050
+ async function jurisdictionSessions(apiKey: string, jurisdiction: string) {
1051
+ const key = jurisdictionPathId(jurisdiction);
1052
+ const hit = sessionCache.get(key);
1053
+ if (hit && Date.now() - hit.at < SESSION_TTL_MS) return hit.data;
1054
+ const data = await osFetch<{ name?: string; legislative_sessions?: OsSession[] }>(
1055
+ apiKey,
1056
+ `/jurisdictions/${key}`,
1057
+ new URLSearchParams({ include: 'legislative_sessions' }),
1058
+ );
1059
+ sessionCache.set(key, { at: Date.now(), data });
1060
+ return data;
1061
+ }
1062
+
1024
1063
  /**
1025
1064
  * Check a session value against the jurisdiction's real session list.
1026
1065
  * exact → it IS a valid identifier (an empty result is genuine).
@@ -1033,11 +1072,7 @@ async function resolveSession(
1033
1072
  jurisdiction: string,
1034
1073
  session: string,
1035
1074
  ): Promise<{ exact?: string; candidates: OsSession[] }> {
1036
- const data = await osFetch<{ name?: string; legislative_sessions?: OsSession[] }>(
1037
- apiKey,
1038
- `/jurisdictions/${jurisdictionPathId(jurisdiction)}`,
1039
- new URLSearchParams({ include: 'legislative_sessions' }),
1040
- );
1075
+ const data = await jurisdictionSessions(apiKey, jurisdiction);
1041
1076
  const sessions = (data.legislative_sessions ?? []).filter((s) => s && s.identifier);
1042
1077
  const norm = session.trim().toLowerCase();
1043
1078
  const exact = sessions.find(
@@ -1062,7 +1097,9 @@ async function resolveSession(
1062
1097
 
1063
1098
  async function getBill(apiKey: string, args: Record<string, unknown>) {
1064
1099
  const id = (args.openstates_id as string | undefined)?.trim();
1065
- const params = new URLSearchParams({ include: 'sponsorships,actions,votes,versions,sources,abstracts' });
1100
+ // v3 takes `include` REPEATED; a comma-joined value is a 422 on every call.
1101
+ const params = new URLSearchParams();
1102
+ for (const inc of ['sponsorships', 'actions', 'votes', 'versions', 'sources', 'abstracts']) params.append('include', inc);
1066
1103
 
1067
1104
  if (id) {
1068
1105
  const data = await osFetch<OsBill>(apiKey, `/bills/${encodeURIComponent(id)}`, params);
package/src/server.ts CHANGED
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
9
9
  import pack from './index.js';
10
10
 
11
11
  const server = new Server(
12
- { name: '@pipeworx/mcp-openstates', version: '0.1.3' },
12
+ { name: '@pipeworx/mcp-openstates', version: '0.1.5' },
13
13
  { capabilities: { tools: {} } },
14
14
  );
15
15