va-lis-mcp 0.1.0__tar.gz
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.
- va_lis_mcp-0.1.0/LICENSE +21 -0
- va_lis_mcp-0.1.0/PKG-INFO +285 -0
- va_lis_mcp-0.1.0/README.md +256 -0
- va_lis_mcp-0.1.0/pyproject.toml +60 -0
- va_lis_mcp-0.1.0/setup.cfg +4 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp/__init__.py +6 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp/_shaping.py +345 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp/asgi.py +18 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp/server.py +1043 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/PKG-INFO +285 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/SOURCES.txt +20 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/dependency_links.txt +1 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/entry_points.txt +2 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/requires.txt +7 -0
- va_lis_mcp-0.1.0/src/va_lis_mcp.egg-info/top_level.txt +1 -0
- va_lis_mcp-0.1.0/tests/test_bills.py +318 -0
- va_lis_mcp-0.1.0/tests/test_committees.py +78 -0
- va_lis_mcp-0.1.0/tests/test_diagnostics.py +60 -0
- va_lis_mcp-0.1.0/tests/test_meetings.py +173 -0
- va_lis_mcp-0.1.0/tests/test_members.py +168 -0
- va_lis_mcp-0.1.0/tests/test_server.py +164 -0
- va_lis_mcp-0.1.0/tests/test_votes.py +224 -0
va_lis_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonny Fuller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: va-lis-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for the Virginia Legislative Information System (LIS) API
|
|
5
|
+
Author-email: Jonny Fuller <mr.waffles@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JonnyWaffles/va-lis-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/JonnyWaffles/va-lis-mcp
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/JonnyWaffles/va-lis-mcp/issues
|
|
10
|
+
Keywords: virginia,legislature,LIS,legislation,API,MCP,model context protocol
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp<3,>=2
|
|
23
|
+
Requires-Dist: va-lis-client<0.4,>=0.3.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=1; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.14; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# va-lis-mcp
|
|
31
|
+
|
|
32
|
+
An MCP server for the **Virginia Legislative Information System (LIS) REST
|
|
33
|
+
API**. It exposes the Virginia General Assembly's sessions, bills, summaries,
|
|
34
|
+
full bill text, action history, votes and roll calls, members, committees,
|
|
35
|
+
and meetings to any MCP client (Claude Code, Claude Desktop, and others).
|
|
36
|
+
Every tool is read only. You bring your own LIS API key.
|
|
37
|
+
|
|
38
|
+
Built on [va-lis-client](https://pypi.org/project/va-lis-client/), a Python
|
|
39
|
+
client for the LIS API.
|
|
40
|
+
|
|
41
|
+
> **Status: not yet on PyPI.** The `uvx va-lis-mcp` snippets below are written
|
|
42
|
+
> for the first release. Until then, install from a source checkout (see
|
|
43
|
+
> [Installing from source](#installing-from-source)).
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Python 3.10 or newer
|
|
48
|
+
- [uv](https://docs.astral.sh/uv/), which provides the `uvx` launcher used
|
|
49
|
+
below. Install it with one line:
|
|
50
|
+
`curl -LsSf https://astral.sh/uv/install.sh | sh` (macOS/Linux) or
|
|
51
|
+
`powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows)
|
|
52
|
+
- A LIS API key (next section)
|
|
53
|
+
|
|
54
|
+
## Getting an API key
|
|
55
|
+
|
|
56
|
+
Register for a free partner key at
|
|
57
|
+
<https://lis.virginia.gov/apiregistration>. The key is a GUID. The server
|
|
58
|
+
reads it from the `LIS_API_KEY` environment variable.
|
|
59
|
+
|
|
60
|
+
LIS heartbeat endpoints accept any key, even garbage, so a successful ping
|
|
61
|
+
proves nothing. Use the `check_api_key` tool to confirm the key is registered
|
|
62
|
+
and active.
|
|
63
|
+
|
|
64
|
+
## Install and configure
|
|
65
|
+
|
|
66
|
+
### Claude Code
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uvx va-lis-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Claude Desktop and other MCP clients
|
|
73
|
+
|
|
74
|
+
Add this to the client's MCP configuration (for Claude Desktop that is
|
|
75
|
+
`claude_desktop_config.json`):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"va-lis": {
|
|
81
|
+
"command": "uvx",
|
|
82
|
+
"args": ["va-lis-mcp"],
|
|
83
|
+
"env": { "LIS_API_KEY": "your-guid-here" }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Installing from source
|
|
90
|
+
|
|
91
|
+
Until the first PyPI release, point the client at a checkout instead:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/JonnyWaffles/va-lis-mcp
|
|
95
|
+
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uv run --directory /path/to/va-lis-mcp va-lis-mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The same works in a JSON config with `"command": "uv"` and
|
|
99
|
+
`"args": ["run", "--directory", "/path/to/va-lis-mcp", "va-lis-mcp"]`.
|
|
100
|
+
|
|
101
|
+
## Tools
|
|
102
|
+
|
|
103
|
+
Every tool returns compact JSON. Bills are addressed by bill number plus
|
|
104
|
+
session code, members by `member_id`, and committees by number (`H08`) or
|
|
105
|
+
name.
|
|
106
|
+
|
|
107
|
+
### Sessions and bills
|
|
108
|
+
|
|
109
|
+
| Tool | Inputs | Returns |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `get_current_session` | none | The GA's working session, with a note on what "current" means during the interim |
|
|
112
|
+
| `list_sessions` | `year` | Sessions for a calendar year with their codes (reference data reaches back to 1994) |
|
|
113
|
+
| `list_bills` | `session_code`, `query?`, `limit?`, `offset?` | Bill rows filtered by a substring match on number, description, and title; paginated, with a total match count |
|
|
114
|
+
| `get_bill` | `bill_number`, `session_code` | Full bill detail: title, status, dates, patrons grouped by role, and the session lineage list |
|
|
115
|
+
| `get_bill_patrons` | `bill_number`, `session_code` | Every patron with member id, role, and chamber, in LIS display order |
|
|
116
|
+
| `get_bill_summaries` | `bill_number`, `session_code` | Plain text summaries (as introduced, as passed); the active entry is the current one |
|
|
117
|
+
| `get_bill_text` | `bill_number`, `session_code`, `document_code?` | Plain text of one version (default: most recent), truncated at 15,000 characters, plus the list of available versions |
|
|
118
|
+
| `get_bill_history` | `bill_number`, `session_code` | Chronological events with date, description, actor, committee, vote tally, `vote_id`, status, and passage flags |
|
|
119
|
+
|
|
120
|
+
### Votes
|
|
121
|
+
|
|
122
|
+
| Tool | Inputs | Returns |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `get_bill_votes` | `bill_number`, `session_code` | Every vote the bill saw in either chamber (floor, committee, subcommittee) with tally, outcome, and the voice, block, and roll call flags |
|
|
125
|
+
| `get_roll_call` | `bill_number`, `session_code`, `vote_id?` | Members grouped as yea, nay, abstain, and not voting, each with party and district, plus any vote statements; voice and block votes are left out |
|
|
126
|
+
| `get_member_votes` | `member_id`, `session_code`, `bill_number?`, `exclude_block_votes?`, `limit?`, `offset?` | One member's votes across a session or on one bill, with block votes flagged; paginated |
|
|
127
|
+
|
|
128
|
+
### Members
|
|
129
|
+
|
|
130
|
+
| Tool | Inputs | Returns |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `list_members` | `session_code`, `query?`, `chamber?` | The session roster, searchable by any part of a name, departed members included |
|
|
133
|
+
| `get_member` | `member_id`, `session_code` | One roster record with party, district, status, and contact details |
|
|
134
|
+
| `get_member_bills` | `member_id`, `session_code`, `role?`, `query?`, `limit?`, `offset?` | Every bill the member patrons, with chief patronage flagged; paginated |
|
|
135
|
+
| `get_member_committees` | `member_id`, `session_code`, `include_subcommittees?` | The member's committee seats and roles |
|
|
136
|
+
|
|
137
|
+
### Committees
|
|
138
|
+
|
|
139
|
+
| Tool | Inputs | Returns |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `list_committees` | `chamber?`, `include_subcommittees?` | The standing committees, with subcommittees on request |
|
|
142
|
+
| `get_committee` | `committee`, `session_code`, `chamber?` | The committee's roster for the session with each member's role, party, and district, its regular meeting note, and its subcommittees |
|
|
143
|
+
|
|
144
|
+
### Meetings
|
|
145
|
+
|
|
146
|
+
| Tool | Inputs | Returns |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `list_meetings` | `start_date`, `end_date`, `committee?`, `chamber?`, `meeting_type?`, `limit?`, `offset?` | The master schedule for a date range: date, the clerk's time text, room, owner, and cancellation flag; paginated |
|
|
149
|
+
| `list_dockets` | `committee`, `session_code` | A Senate committee's dockets with the bills on each (the House keeps none) |
|
|
150
|
+
| `list_floor_calendars` | `chamber`, `session_code` | A chamber's floor calendars for the session |
|
|
151
|
+
| `get_floor_calendar` | `calendar_id` | The bills on one floor calendar by section, with the actions and vote ids recorded against each |
|
|
152
|
+
|
|
153
|
+
### Diagnostics
|
|
154
|
+
|
|
155
|
+
| Tool | Inputs | Returns |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `check_api_key` | none | Whether the configured key is registered and active |
|
|
158
|
+
|
|
159
|
+
Errors come back as readable tool errors. A bill lookup that misses names the
|
|
160
|
+
bill and session it searched; an ambiguous committee name lists the matches;
|
|
161
|
+
an unknown member id points at `list_members`; a missing key tells you to set
|
|
162
|
+
`LIS_API_KEY` and where to register.
|
|
163
|
+
|
|
164
|
+
## Domain notes
|
|
165
|
+
|
|
166
|
+
Distilled from the [va-lis-client
|
|
167
|
+
documentation](https://github.com/JonnyWaffles/va-lis-client), which is the
|
|
168
|
+
full reference for LIS behavior.
|
|
169
|
+
|
|
170
|
+
- **Session codes** encode year plus sequence: `20261` is the 2026 Regular
|
|
171
|
+
Session, `20262` is 2026 Special Session I. Most tools require one.
|
|
172
|
+
- **Bill numbers are unpadded.** LIS returns nothing for `HB0001`; the
|
|
173
|
+
correct form is `HB1`. The tools normalize input (`hb0001` becomes `HB1`),
|
|
174
|
+
so you can pass either.
|
|
175
|
+
- **The default session tracks the GA's working session.** Once a session
|
|
176
|
+
wraps up, the default advances to the upcoming session during the interim.
|
|
177
|
+
By August 2026 it already pointed at `20271`. For retrospective work on a
|
|
178
|
+
session that just ended, pass its explicit session code.
|
|
179
|
+
- **Carry over reuses bill IDs.** A bill from an even year session may be
|
|
180
|
+
carried over into the following odd year session (never the reverse, at
|
|
181
|
+
most once). The carried bill keeps its numeric `LegislationID` in both
|
|
182
|
+
sessions, so a bill is identified by session code plus bill number. The
|
|
183
|
+
`sessions` list on `get_bill` is the lineage record: two entries mean the
|
|
184
|
+
bill was carried over.
|
|
185
|
+
- **Passage dates live in the history.** The bill detail's passage date
|
|
186
|
+
fields are always null in LIS; `get_bill_history` flags passage events and
|
|
187
|
+
carries the vote tallies.
|
|
188
|
+
- **Votes reach a bill through its history.** No LIS vote endpoint accepts a
|
|
189
|
+
bill id, so `get_bill_votes` walks the bill's events and fetches the vote
|
|
190
|
+
behind each one. `get_bill_history` carries the `vote_id` on each event.
|
|
191
|
+
- **A block vote is not a position on one bill.** A chamber bundles its
|
|
192
|
+
uncontroversial bills and passes them in one motion; one 2026 House vote
|
|
193
|
+
disposed of 105 bills at 97 to 0. A voice vote records no members at all.
|
|
194
|
+
`get_roll_call` leaves both out, `get_bill_votes` flags them, and
|
|
195
|
+
`get_member_votes` marks each row with `is_block`.
|
|
196
|
+
- **Not voting is not in the tally.** Members recorded as `not_voting` (LIS
|
|
197
|
+
code `X`) are omitted from the tally string, so the member groups never add
|
|
198
|
+
up to it. Vote statements are corrections a member filed afterwards; the
|
|
199
|
+
recorded roll call is never amended.
|
|
200
|
+
- **The roster is larger than the chamber, on purpose.** A session roster
|
|
201
|
+
includes members who left or arrived mid-session, because they cast the
|
|
202
|
+
votes recorded before they left. `list_members` keeps them and marks the
|
|
203
|
+
departure date and reason.
|
|
204
|
+
- **Committees are not session scoped; their seats are.** The committee
|
|
205
|
+
list describes the committees that exist now, and `get_committee` reads the
|
|
206
|
+
seat list for the session you name. LIS has no "bills referred to a
|
|
207
|
+
committee" endpoint; `get_bill_history` names the committee that holds a
|
|
208
|
+
bill.
|
|
209
|
+
- **Dockets are Senate only.** House committee agendas are not in the API:
|
|
210
|
+
`list_meetings` gives the meeting, the bill's history gives the referral,
|
|
211
|
+
and the agenda itself lives on the committee's web page. Schedule times are
|
|
212
|
+
free text as the clerk entered them and may be blank.
|
|
213
|
+
|
|
214
|
+
## Embedding in an ASGI app
|
|
215
|
+
|
|
216
|
+
Besides stdio, the server can be served over streamable HTTP. The packaged
|
|
217
|
+
ASGI app is stateless and returns plain JSON, so it works behind multiple
|
|
218
|
+
workers without sticky sessions:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uvicorn va_lis_mcp.asgi:app --workers 4
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The MCP endpoint is at `/mcp`. To mount it inside a larger Starlette (or any
|
|
225
|
+
ASGI) application, the host app's lifespan must run the MCP session manager:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from contextlib import asynccontextmanager
|
|
229
|
+
|
|
230
|
+
from starlette.applications import Starlette
|
|
231
|
+
from starlette.routing import Mount
|
|
232
|
+
|
|
233
|
+
from va_lis_mcp.server import mcp
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@asynccontextmanager
|
|
237
|
+
async def lifespan(app):
|
|
238
|
+
async with mcp.session_manager.run():
|
|
239
|
+
yield
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
app = Starlette(
|
|
243
|
+
routes=[
|
|
244
|
+
Mount("/lis", app=mcp.streamable_http_app(stateless_http=True, json_response=True)),
|
|
245
|
+
],
|
|
246
|
+
lifespan=lifespan,
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
That serves the endpoint at `/lis/mcp`. Keep `stateless_http=True` whenever
|
|
251
|
+
more than one worker handles requests: the default stateful transport needs
|
|
252
|
+
every request of a session to reach the same worker, which a plain worker
|
|
253
|
+
pool does not guarantee.
|
|
254
|
+
|
|
255
|
+
This is ASGI only; a WSGI stack cannot host it. Django projects can still
|
|
256
|
+
serve it, but through their ASGI entry point (for example gunicorn with
|
|
257
|
+
uvicorn workers), not through WSGI.
|
|
258
|
+
|
|
259
|
+
## Development
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
uv venv
|
|
263
|
+
uv pip install -e ".[dev]"
|
|
264
|
+
uv run pytest
|
|
265
|
+
uv run ruff check .
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Poke at the tools interactively with the MCP Inspector:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npx @modelcontextprotocol/inspector uv run va-lis-mcp
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Upgrading
|
|
275
|
+
|
|
276
|
+
`uvx` caches resolved environments, so after a new release force resolution
|
|
277
|
+
of the latest version:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
uvx va-lis-mcp@latest
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# va-lis-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server for the **Virginia Legislative Information System (LIS) REST
|
|
4
|
+
API**. It exposes the Virginia General Assembly's sessions, bills, summaries,
|
|
5
|
+
full bill text, action history, votes and roll calls, members, committees,
|
|
6
|
+
and meetings to any MCP client (Claude Code, Claude Desktop, and others).
|
|
7
|
+
Every tool is read only. You bring your own LIS API key.
|
|
8
|
+
|
|
9
|
+
Built on [va-lis-client](https://pypi.org/project/va-lis-client/), a Python
|
|
10
|
+
client for the LIS API.
|
|
11
|
+
|
|
12
|
+
> **Status: not yet on PyPI.** The `uvx va-lis-mcp` snippets below are written
|
|
13
|
+
> for the first release. Until then, install from a source checkout (see
|
|
14
|
+
> [Installing from source](#installing-from-source)).
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Python 3.10 or newer
|
|
19
|
+
- [uv](https://docs.astral.sh/uv/), which provides the `uvx` launcher used
|
|
20
|
+
below. Install it with one line:
|
|
21
|
+
`curl -LsSf https://astral.sh/uv/install.sh | sh` (macOS/Linux) or
|
|
22
|
+
`powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows)
|
|
23
|
+
- A LIS API key (next section)
|
|
24
|
+
|
|
25
|
+
## Getting an API key
|
|
26
|
+
|
|
27
|
+
Register for a free partner key at
|
|
28
|
+
<https://lis.virginia.gov/apiregistration>. The key is a GUID. The server
|
|
29
|
+
reads it from the `LIS_API_KEY` environment variable.
|
|
30
|
+
|
|
31
|
+
LIS heartbeat endpoints accept any key, even garbage, so a successful ping
|
|
32
|
+
proves nothing. Use the `check_api_key` tool to confirm the key is registered
|
|
33
|
+
and active.
|
|
34
|
+
|
|
35
|
+
## Install and configure
|
|
36
|
+
|
|
37
|
+
### Claude Code
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uvx va-lis-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Claude Desktop and other MCP clients
|
|
44
|
+
|
|
45
|
+
Add this to the client's MCP configuration (for Claude Desktop that is
|
|
46
|
+
`claude_desktop_config.json`):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"va-lis": {
|
|
52
|
+
"command": "uvx",
|
|
53
|
+
"args": ["va-lis-mcp"],
|
|
54
|
+
"env": { "LIS_API_KEY": "your-guid-here" }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Installing from source
|
|
61
|
+
|
|
62
|
+
Until the first PyPI release, point the client at a checkout instead:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/JonnyWaffles/va-lis-mcp
|
|
66
|
+
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uv run --directory /path/to/va-lis-mcp va-lis-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The same works in a JSON config with `"command": "uv"` and
|
|
70
|
+
`"args": ["run", "--directory", "/path/to/va-lis-mcp", "va-lis-mcp"]`.
|
|
71
|
+
|
|
72
|
+
## Tools
|
|
73
|
+
|
|
74
|
+
Every tool returns compact JSON. Bills are addressed by bill number plus
|
|
75
|
+
session code, members by `member_id`, and committees by number (`H08`) or
|
|
76
|
+
name.
|
|
77
|
+
|
|
78
|
+
### Sessions and bills
|
|
79
|
+
|
|
80
|
+
| Tool | Inputs | Returns |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `get_current_session` | none | The GA's working session, with a note on what "current" means during the interim |
|
|
83
|
+
| `list_sessions` | `year` | Sessions for a calendar year with their codes (reference data reaches back to 1994) |
|
|
84
|
+
| `list_bills` | `session_code`, `query?`, `limit?`, `offset?` | Bill rows filtered by a substring match on number, description, and title; paginated, with a total match count |
|
|
85
|
+
| `get_bill` | `bill_number`, `session_code` | Full bill detail: title, status, dates, patrons grouped by role, and the session lineage list |
|
|
86
|
+
| `get_bill_patrons` | `bill_number`, `session_code` | Every patron with member id, role, and chamber, in LIS display order |
|
|
87
|
+
| `get_bill_summaries` | `bill_number`, `session_code` | Plain text summaries (as introduced, as passed); the active entry is the current one |
|
|
88
|
+
| `get_bill_text` | `bill_number`, `session_code`, `document_code?` | Plain text of one version (default: most recent), truncated at 15,000 characters, plus the list of available versions |
|
|
89
|
+
| `get_bill_history` | `bill_number`, `session_code` | Chronological events with date, description, actor, committee, vote tally, `vote_id`, status, and passage flags |
|
|
90
|
+
|
|
91
|
+
### Votes
|
|
92
|
+
|
|
93
|
+
| Tool | Inputs | Returns |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `get_bill_votes` | `bill_number`, `session_code` | Every vote the bill saw in either chamber (floor, committee, subcommittee) with tally, outcome, and the voice, block, and roll call flags |
|
|
96
|
+
| `get_roll_call` | `bill_number`, `session_code`, `vote_id?` | Members grouped as yea, nay, abstain, and not voting, each with party and district, plus any vote statements; voice and block votes are left out |
|
|
97
|
+
| `get_member_votes` | `member_id`, `session_code`, `bill_number?`, `exclude_block_votes?`, `limit?`, `offset?` | One member's votes across a session or on one bill, with block votes flagged; paginated |
|
|
98
|
+
|
|
99
|
+
### Members
|
|
100
|
+
|
|
101
|
+
| Tool | Inputs | Returns |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `list_members` | `session_code`, `query?`, `chamber?` | The session roster, searchable by any part of a name, departed members included |
|
|
104
|
+
| `get_member` | `member_id`, `session_code` | One roster record with party, district, status, and contact details |
|
|
105
|
+
| `get_member_bills` | `member_id`, `session_code`, `role?`, `query?`, `limit?`, `offset?` | Every bill the member patrons, with chief patronage flagged; paginated |
|
|
106
|
+
| `get_member_committees` | `member_id`, `session_code`, `include_subcommittees?` | The member's committee seats and roles |
|
|
107
|
+
|
|
108
|
+
### Committees
|
|
109
|
+
|
|
110
|
+
| Tool | Inputs | Returns |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `list_committees` | `chamber?`, `include_subcommittees?` | The standing committees, with subcommittees on request |
|
|
113
|
+
| `get_committee` | `committee`, `session_code`, `chamber?` | The committee's roster for the session with each member's role, party, and district, its regular meeting note, and its subcommittees |
|
|
114
|
+
|
|
115
|
+
### Meetings
|
|
116
|
+
|
|
117
|
+
| Tool | Inputs | Returns |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `list_meetings` | `start_date`, `end_date`, `committee?`, `chamber?`, `meeting_type?`, `limit?`, `offset?` | The master schedule for a date range: date, the clerk's time text, room, owner, and cancellation flag; paginated |
|
|
120
|
+
| `list_dockets` | `committee`, `session_code` | A Senate committee's dockets with the bills on each (the House keeps none) |
|
|
121
|
+
| `list_floor_calendars` | `chamber`, `session_code` | A chamber's floor calendars for the session |
|
|
122
|
+
| `get_floor_calendar` | `calendar_id` | The bills on one floor calendar by section, with the actions and vote ids recorded against each |
|
|
123
|
+
|
|
124
|
+
### Diagnostics
|
|
125
|
+
|
|
126
|
+
| Tool | Inputs | Returns |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `check_api_key` | none | Whether the configured key is registered and active |
|
|
129
|
+
|
|
130
|
+
Errors come back as readable tool errors. A bill lookup that misses names the
|
|
131
|
+
bill and session it searched; an ambiguous committee name lists the matches;
|
|
132
|
+
an unknown member id points at `list_members`; a missing key tells you to set
|
|
133
|
+
`LIS_API_KEY` and where to register.
|
|
134
|
+
|
|
135
|
+
## Domain notes
|
|
136
|
+
|
|
137
|
+
Distilled from the [va-lis-client
|
|
138
|
+
documentation](https://github.com/JonnyWaffles/va-lis-client), which is the
|
|
139
|
+
full reference for LIS behavior.
|
|
140
|
+
|
|
141
|
+
- **Session codes** encode year plus sequence: `20261` is the 2026 Regular
|
|
142
|
+
Session, `20262` is 2026 Special Session I. Most tools require one.
|
|
143
|
+
- **Bill numbers are unpadded.** LIS returns nothing for `HB0001`; the
|
|
144
|
+
correct form is `HB1`. The tools normalize input (`hb0001` becomes `HB1`),
|
|
145
|
+
so you can pass either.
|
|
146
|
+
- **The default session tracks the GA's working session.** Once a session
|
|
147
|
+
wraps up, the default advances to the upcoming session during the interim.
|
|
148
|
+
By August 2026 it already pointed at `20271`. For retrospective work on a
|
|
149
|
+
session that just ended, pass its explicit session code.
|
|
150
|
+
- **Carry over reuses bill IDs.** A bill from an even year session may be
|
|
151
|
+
carried over into the following odd year session (never the reverse, at
|
|
152
|
+
most once). The carried bill keeps its numeric `LegislationID` in both
|
|
153
|
+
sessions, so a bill is identified by session code plus bill number. The
|
|
154
|
+
`sessions` list on `get_bill` is the lineage record: two entries mean the
|
|
155
|
+
bill was carried over.
|
|
156
|
+
- **Passage dates live in the history.** The bill detail's passage date
|
|
157
|
+
fields are always null in LIS; `get_bill_history` flags passage events and
|
|
158
|
+
carries the vote tallies.
|
|
159
|
+
- **Votes reach a bill through its history.** No LIS vote endpoint accepts a
|
|
160
|
+
bill id, so `get_bill_votes` walks the bill's events and fetches the vote
|
|
161
|
+
behind each one. `get_bill_history` carries the `vote_id` on each event.
|
|
162
|
+
- **A block vote is not a position on one bill.** A chamber bundles its
|
|
163
|
+
uncontroversial bills and passes them in one motion; one 2026 House vote
|
|
164
|
+
disposed of 105 bills at 97 to 0. A voice vote records no members at all.
|
|
165
|
+
`get_roll_call` leaves both out, `get_bill_votes` flags them, and
|
|
166
|
+
`get_member_votes` marks each row with `is_block`.
|
|
167
|
+
- **Not voting is not in the tally.** Members recorded as `not_voting` (LIS
|
|
168
|
+
code `X`) are omitted from the tally string, so the member groups never add
|
|
169
|
+
up to it. Vote statements are corrections a member filed afterwards; the
|
|
170
|
+
recorded roll call is never amended.
|
|
171
|
+
- **The roster is larger than the chamber, on purpose.** A session roster
|
|
172
|
+
includes members who left or arrived mid-session, because they cast the
|
|
173
|
+
votes recorded before they left. `list_members` keeps them and marks the
|
|
174
|
+
departure date and reason.
|
|
175
|
+
- **Committees are not session scoped; their seats are.** The committee
|
|
176
|
+
list describes the committees that exist now, and `get_committee` reads the
|
|
177
|
+
seat list for the session you name. LIS has no "bills referred to a
|
|
178
|
+
committee" endpoint; `get_bill_history` names the committee that holds a
|
|
179
|
+
bill.
|
|
180
|
+
- **Dockets are Senate only.** House committee agendas are not in the API:
|
|
181
|
+
`list_meetings` gives the meeting, the bill's history gives the referral,
|
|
182
|
+
and the agenda itself lives on the committee's web page. Schedule times are
|
|
183
|
+
free text as the clerk entered them and may be blank.
|
|
184
|
+
|
|
185
|
+
## Embedding in an ASGI app
|
|
186
|
+
|
|
187
|
+
Besides stdio, the server can be served over streamable HTTP. The packaged
|
|
188
|
+
ASGI app is stateless and returns plain JSON, so it works behind multiple
|
|
189
|
+
workers without sticky sessions:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uvicorn va_lis_mcp.asgi:app --workers 4
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The MCP endpoint is at `/mcp`. To mount it inside a larger Starlette (or any
|
|
196
|
+
ASGI) application, the host app's lifespan must run the MCP session manager:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from contextlib import asynccontextmanager
|
|
200
|
+
|
|
201
|
+
from starlette.applications import Starlette
|
|
202
|
+
from starlette.routing import Mount
|
|
203
|
+
|
|
204
|
+
from va_lis_mcp.server import mcp
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@asynccontextmanager
|
|
208
|
+
async def lifespan(app):
|
|
209
|
+
async with mcp.session_manager.run():
|
|
210
|
+
yield
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
app = Starlette(
|
|
214
|
+
routes=[
|
|
215
|
+
Mount("/lis", app=mcp.streamable_http_app(stateless_http=True, json_response=True)),
|
|
216
|
+
],
|
|
217
|
+
lifespan=lifespan,
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
That serves the endpoint at `/lis/mcp`. Keep `stateless_http=True` whenever
|
|
222
|
+
more than one worker handles requests: the default stateful transport needs
|
|
223
|
+
every request of a session to reach the same worker, which a plain worker
|
|
224
|
+
pool does not guarantee.
|
|
225
|
+
|
|
226
|
+
This is ASGI only; a WSGI stack cannot host it. Django projects can still
|
|
227
|
+
serve it, but through their ASGI entry point (for example gunicorn with
|
|
228
|
+
uvicorn workers), not through WSGI.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
uv venv
|
|
234
|
+
uv pip install -e ".[dev]"
|
|
235
|
+
uv run pytest
|
|
236
|
+
uv run ruff check .
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Poke at the tools interactively with the MCP Inspector:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx @modelcontextprotocol/inspector uv run va-lis-mcp
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Upgrading
|
|
246
|
+
|
|
247
|
+
`uvx` caches resolved environments, so after a new release force resolution
|
|
248
|
+
of the latest version:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
uvx va-lis-mcp@latest
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "va-lis-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for the Virginia Legislative Information System (LIS) API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "Jonny Fuller", email = "mr.waffles@gmail.com" }]
|
|
9
|
+
keywords = ["virginia", "legislature", "LIS", "legislation", "API", "MCP", "model context protocol"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=2,<3",
|
|
22
|
+
"va-lis-client>=0.3.0,<0.4",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
# Floors are real claims: CI runs a --resolution lowest-direct job against
|
|
27
|
+
# them. pytest-asyncio 1.x needs pytest >= 8.2, and asyncio_mode = "auto"
|
|
28
|
+
# below needs pytest-asyncio >= 1.
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.2",
|
|
31
|
+
"pytest-asyncio>=1",
|
|
32
|
+
"ruff>=0.14",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
va-lis-mcp = "va_lis_mcp.server:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/JonnyWaffles/va-lis-mcp"
|
|
40
|
+
Repository = "https://github.com/JonnyWaffles/va-lis-mcp"
|
|
41
|
+
"Bug Tracker" = "https://github.com/JonnyWaffles/va-lis-mcp/issues"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["setuptools>=68"]
|
|
45
|
+
build-backend = "setuptools.build_meta"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
# pytest-asyncio warns when this is unset and will change its default later.
|
|
54
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
lint.select = ["E", "F", "B", "I"]
|
|
59
|
+
lint.extend-select = ["I"]
|
|
60
|
+
fix = true
|