Development
This guide explains how bluelink-mcp is put together and how to change it. For how to contribute, see the contributing guide.
You need Python 3.12+ and uv. Docker is optional.
git clone https://github.com/<you>/bluelink-mcp.gitcd bluelink-mcpuv syncuv run pytestChecks that CI runs:
uv run pytest # testsuv run ruff check . && uv run ruff format --check .uv run python scripts/gen_tool_docs.py --check # docs/tools.md matches the codedocker build . # optional: tests run inside the build tooArchitecture
Section titled “Architecture”src/bluelink_mcp/├── __main__.py entry point: config, stdio or HTTP, bearer auth, /health├── config.py environment variables → a frozen Config├── client.py BluelinkClient: session, token cache, rate limits, commands├── format.py library objects → compact JSON for the model└── server.py the MCP toolsscripts/└── gen_tool_docs.py writes docs/tools.md from the tool definitionstests/└── test_server.py tests against a fake VehicleManagerHow a request flows:
- The MCP SDK (
MCPServer, from the officialmcppackage) calls a tool function inserver.py. - The tool calls
BluelinkClientin a worker thread, becausehyundai_kia_connect_apiis synchronous. BluelinkClient.call()takes a lock, makes sure there’s a valid session (loading the cached token, logging in or refreshing as needed), runs the library call and saves the token if it changed.- The tool formats the result with
format.py, which drops fields the car doesn’t report.
Errors meant for the model are raised as BluelinkError, a subclass of the SDK’s ToolError, so the model sees the message. Any other exception reaches the model only as a generic error, with the details in the server log.
Adding a tool
Section titled “Adding a tool”- Check the library. Find the
VehicleManagermethod and see which regions implement it. - Add the tool in
server.pywith@mcp.tool(title=..., annotations=...):- Use
READfor reads,WRITEfor commands andSENSITIVEfor anything with security impact. - Register command tools only inside the
allow_writessection. This gating is the project’s main safety feature, so reviewers check it carefully. - Take
vehicle: VehicleArg = None, andwait: WaitArg = Truefor commands. - Send commands through
client.command(...), which resolves the vehicle, clears the cache and waits for the result. - Describe every argument with
Field(description=...), and write the docstring for the model: what it does, side effects and units.
- Use
- Add it to a group in
scripts/gen_tool_docs.py. The script fails if a tool is in no group. - Test it in
tests/test_server.py, adding the method toFakeManager. - Regenerate the docs:
uv run python scripts/gen_tool_docs.py. - Add a changelog entry under Unreleased.
Testing
Section titled “Testing”The tests never touch Bluelink. FakeManager stands in for VehicleManager: it has one car with some fields set, records every call, and returns canned action IDs and statuses. make() builds a server with a temporary token file, and call() runs a tool through the real MCP tool machinery, so argument validation and error handling are tested too.
Trying it against a real car
Section titled “Trying it against a real car”Use the MCP Inspector:
BLUELINK_USERNAME=... BLUELINK_PASSWORD=... BLUELINK_PIN=... \ npx @modelcontextprotocol/inspector uv run bluelink-mcpGo easy on refresh_vehicle_status and commands while developing. Every refresh wakes the car, and Bluelink limits API calls per day.
To see the library’s requests, set Python logging to debug for hyundai_kia_connect_api. Never paste that output publicly: it contains tokens and your car’s VIN and location.
The website
Section titled “The website”The site at bluelink-mcp.mattoddie.dev is in website/. It’s built with Astro and Starlight from the Markdown in docs/, so edit the docs here and the site follows. See website/README.md.