va-lis-mcp
An MCP server for the Virginia Legislative Information System (LIS) REST API. It exposes the Virginia General Assembly's sessions, bills, summaries, full bill text, action history, votes and roll calls, members, committees, and meetings to any MCP client (Claude Code, Claude Desktop, and others). Every tool is read only. You bring your own LIS API key.
Built on va-lis-client, a Python client for the LIS API.
Status: not yet on PyPI. The
uvx va-lis-mcpsnippets below are written for the first release. Until then, install from a source checkout (see Installing from source).
Requirements
- Python 3.10 or newer
- uv, which provides the
uvxlauncher used below. Install it with one line:curl -LsSf https://astral.sh/uv/install.sh | sh(macOS/Linux) orpowershell -c "irm https://astral.sh/uv/install.ps1 | iex"(Windows) - A LIS API key (next section)
Getting an API key
Register for a free partner key at
https://lis.virginia.gov/apiregistration. The key is a GUID. The server
reads it from the LIS_API_KEY environment variable.
LIS heartbeat endpoints accept any key, even garbage, so a successful ping
proves nothing. Use the check_api_key tool to confirm the key is registered
and active.
Install and configure
Claude Code
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uvx va-lis-mcp
Claude Desktop and other MCP clients
Add this to the client's MCP configuration (for Claude Desktop that is
claude_desktop_config.json):
{
"mcpServers": {
"va-lis": {
"command": "uvx",
"args": ["va-lis-mcp"],
"env": { "LIS_API_KEY": "your-guid-here" }
}
}
}
Installing from source
Until the first PyPI release, point the client at a checkout instead:
git clone https://github.com/JonnyWaffles/va-lis-mcp
claude mcp add va-lis -e LIS_API_KEY=your-guid-here -- uv run --directory /path/to/va-lis-mcp va-lis-mcp
The same works in a JSON config with "command": "uv" and
"args": ["run", "--directory", "/path/to/va-lis-mcp", "va-lis-mcp"].
Tools
Every tool returns compact JSON. Bills are addressed by bill number plus
session code, members by member_id, and committees by number (H08) or
name.
Sessions and bills
| Tool | Inputs | Returns |
|---|---|---|
get_current_session |
none | The GA's working session, with a note on what "current" means during the interim |
list_sessions |
year |
Sessions for a calendar year with their codes (reference data reaches back to 1994) |
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 |
get_bill |
bill_number, session_code |
Full bill detail: title, status, dates, patrons grouped by role, and the session lineage list |
get_bill_patrons |
bill_number, session_code |
Every patron with member id, role, and chamber, in LIS display order |
get_bill_summaries |
bill_number, session_code |
Plain text summaries (as introduced, as passed); the active entry is the current one |
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 |
get_bill_history |
bill_number, session_code |
Chronological events with date, description, actor, committee, vote tally, vote_id, status, and passage flags |
Votes
| Tool | Inputs | Returns |
|---|---|---|
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 |
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 |
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 |
Members
| Tool | Inputs | Returns |
|---|---|---|
list_members |
session_code, query?, chamber? |
The session roster, searchable by any part of a name, departed members included |
get_member |
member_id, session_code |
One roster record with party, district, status, and contact details |
get_member_bills |
member_id, session_code, role?, query?, limit?, offset? |
Every bill the member patrons, with chief patronage flagged; paginated |
get_member_committees |
member_id, session_code, include_subcommittees? |
The member's committee seats and roles |
Committees
| Tool | Inputs | Returns |
|---|---|---|
list_committees |
chamber?, include_subcommittees? |
The standing committees, with subcommittees on request |
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 |
Meetings
| Tool | Inputs | Returns |
|---|---|---|
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 |
list_dockets |
committee, session_code |
A Senate committee's dockets with the bills on each (the House keeps none) |
list_floor_calendars |
chamber, session_code |
A chamber's floor calendars for the session |
get_floor_calendar |
calendar_id |
The bills on one floor calendar by section, with the actions and vote ids recorded against each |
Diagnostics
| Tool | Inputs | Returns |
|---|---|---|
check_api_key |
none | Whether the configured key is registered and active |
Errors come back as readable tool errors. A bill lookup that misses names the
bill and session it searched; an ambiguous committee name lists the matches;
an unknown member id points at list_members; a missing key tells you to set
LIS_API_KEY and where to register.
Domain notes
Distilled from the va-lis-client documentation, which is the full reference for LIS behavior.
- Session codes encode year plus sequence:
20261is the 2026 Regular Session,20262is 2026 Special Session I. Most tools require one. - Bill numbers are unpadded. LIS returns nothing for
HB0001; the correct form isHB1. The tools normalize input (hb0001becomesHB1), so you can pass either. - The default session tracks the GA's working session. Once a session
wraps up, the default advances to the upcoming session during the interim.
By August 2026 it already pointed at
20271. For retrospective work on a session that just ended, pass its explicit session code. - Carry over reuses bill IDs. A bill from an even year session may be
carried over into the following odd year session (never the reverse, at
most once). The carried bill keeps its numeric
LegislationIDin both sessions, so a bill is identified by session code plus bill number. Thesessionslist onget_billis the lineage record: two entries mean the bill was carried over. - Passage dates live in the history. The bill detail's passage date
fields are always null in LIS;
get_bill_historyflags passage events and carries the vote tallies. - Votes reach a bill through its history. No LIS vote endpoint accepts a
bill id, so
get_bill_voteswalks the bill's events and fetches the vote behind each one.get_bill_historycarries thevote_idon each event. - A block vote is not a position on one bill. A chamber bundles its
uncontroversial bills and passes them in one motion; one 2026 House vote
disposed of 105 bills at 97 to 0. A voice vote records no members at all.
get_roll_callleaves both out,get_bill_votesflags them, andget_member_votesmarks each row withis_block. - Not voting is not in the tally. Members recorded as
not_voting(LIS codeX) are omitted from the tally string, so the member groups never add up to it. Vote statements are corrections a member filed afterwards; the recorded roll call is never amended. - The roster is larger than the chamber, on purpose. A session roster
includes members who left or arrived mid-session, because they cast the
votes recorded before they left.
list_memberskeeps them and marks the departure date and reason. - Committees are not session scoped; their seats are. The committee
list describes the committees that exist now, and
get_committeereads the seat list for the session you name. LIS has no "bills referred to a committee" endpoint;get_bill_historynames the committee that holds a bill. - Dockets are Senate only. House committee agendas are not in the API:
list_meetingsgives the meeting, the bill's history gives the referral, and the agenda itself lives on the committee's web page. Schedule times are free text as the clerk entered them and may be blank.
Embedding in an ASGI app
Besides stdio, the server can be served over streamable HTTP. The packaged ASGI app is stateless and returns plain JSON, so it works behind multiple workers without sticky sessions:
uvicorn va_lis_mcp.asgi:app --workers 4
The MCP endpoint is at /mcp. To mount it inside a larger Starlette (or any
ASGI) application, the host app's lifespan must run the MCP session manager:
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.routing import Mount
from va_lis_mcp.server import mcp
@asynccontextmanager
async def lifespan(app):
async with mcp.session_manager.run():
yield
app = Starlette(
routes=[
Mount("/lis", app=mcp.streamable_http_app(stateless_http=True, json_response=True)),
],
lifespan=lifespan,
)
That serves the endpoint at /lis/mcp. Keep stateless_http=True whenever
more than one worker handles requests: the default stateful transport needs
every request of a session to reach the same worker, which a plain worker
pool does not guarantee.
This is ASGI only; a WSGI stack cannot host it. Django projects can still serve it, but through their ASGI entry point (for example gunicorn with uvicorn workers), not through WSGI.
Development
uv venv
uv pip install -e ".[dev]"
uv run pytest
uv run ruff check .
Poke at the tools interactively with the MCP Inspector:
npx @modelcontextprotocol/inspector uv run va-lis-mcp
Upgrading
uvx caches resolved environments, so after a new release force resolution
of the latest version:
uvx va-lis-mcp@latest
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file va_lis_mcp-0.1.0.tar.gz.
File metadata
- Download URL: va_lis_mcp-0.1.0.tar.gz
- Upload date:
- Size: 33.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2881c51d8108c7c7d3150e325b69208ae13e9571b5852d99430636dafcab8f6a
|
|
| MD5 |
569a3f6423c3994e003200c3fa563733
|
|
| BLAKE2b-256 |
c4500945af133124ecb8dde04df279a822a1b191e0d93e260e850488d52d721e
|
Provenance
The following attestation bundles were made for va_lis_mcp-0.1.0.tar.gz:
Publisher:
publish.yml on JonnyWaffles/va-lis-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
va_lis_mcp-0.1.0.tar.gz -
Subject digest:
2881c51d8108c7c7d3150e325b69208ae13e9571b5852d99430636dafcab8f6a - Sigstore transparency entry: 2834619901
- Sigstore integration time:
-
Permalink:
JonnyWaffles/va-lis-mcp@a58452fc72423672021aa490167224c5bae93063 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JonnyWaffles
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a58452fc72423672021aa490167224c5bae93063 -
Trigger Event:
release
-
Statement type:
File details
Details for the file va_lis_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: va_lis_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5382d6f7c93ad360e69cf17c2642c97768dfd719ae1bc2cf332ef4b9ca854447
|
|
| MD5 |
026ac93f6e28805d7f14db1e8f984633
|
|
| BLAKE2b-256 |
2b4bd17a353526b7238fc01cf27bd35c1c7a2c95ad6f2d19753f0e522c191402
|
Provenance
The following attestation bundles were made for va_lis_mcp-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on JonnyWaffles/va-lis-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
va_lis_mcp-0.1.0-py3-none-any.whl -
Subject digest:
5382d6f7c93ad360e69cf17c2642c97768dfd719ae1bc2cf332ef4b9ca854447 - Sigstore transparency entry: 2834620020
- Sigstore integration time:
-
Permalink:
JonnyWaffles/va-lis-mcp@a58452fc72423672021aa490167224c5bae93063 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JonnyWaffles
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a58452fc72423672021aa490167224c5bae93063 -
Trigger Event:
release
-
Statement type: