Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test and build
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: npm
- run: npm ci
- run: npm test -- --runInBand
- run: npm run build
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ After cloning + building, register the server in your client's MCP config. For C
"mcpServers": {
"lua-platform": {
"command": "node",
"args": ["/absolute/path/to/lua-platform-mcp/dist/server.js"],
"env": {
"LUA_API_KEY": "lk_..."
}
"args": ["/absolute/path/to/lua-platform-mcp/dist/server.js"]
}
}
}
Expand Down Expand Up @@ -60,13 +57,17 @@ The server reads your Lua API key in this order:
2. `~/.lua-cli/credentials` (written by `lua auth configure`)
3. `.env` file in CWD

Get a key from [admin.heylua.ai](https://admin.heylua.ai) (sign up + create an agent → Settings → API Keys), or use the Lua CLI:
Credentials are opaque to this MCP server: it selects the first available source and forwards the value unchanged. Never paste a credential into an MCP request or a model conversation. Configure it locally with the Lua CLI, your shell environment, or a local `.env` file instead.

For the copy-paste-safe local setup, use the Lua CLI before starting your MCP client:

```bash
npm install -g lua-cli
lua auth configure
```

That writes `~/.lua-cli/credentials`, which the server reads automatically. Alternatively, create a credential in [admin.heylua.ai](https://admin.heylua.ai) and set its real value in the MCP host's local environment.

## Verify it works

After registering, restart your MCP client and ask: *"What lua-platform tools do you have?"*
Expand Down
26 changes: 12 additions & 14 deletions llms-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,22 @@ This file gives Cline (or any AI agent installing this MCP server on a user's be
3. Register the server in the MCP config file. For Cline that's `cline_mcp_settings.json` (location varies — typically `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` on macOS or analogous paths on other OSes). Add or merge:
```json
{
"mcpServers": {
"lua-platform": {
"command": "node",
"args": ["/absolute/path/to/lua-platform-mcp/dist/server.js"],
"env": {
"LUA_API_KEY": "${env:LUA_API_KEY}"
}
}
"mcpServers": {
"lua-platform": {
"command": "node",
"args": ["/absolute/path/to/lua-platform-mcp/dist/server.js"]
}
}
}
```
Use the absolute path — relative paths are unreliable across MCP client implementations.

4. Ask the user to obtain a Lua API key:
- Visit https://admin.heylua.ai
- Sign up (free) and create an agent
- Settings → API Keys → copy the key (`lk_...`)
- Set it as an environment variable: `export LUA_API_KEY=lk_...` (or place in `~/.lua-cli/credentials`)
4. Keep credentials on the user's machine. Do **not** ask the user to paste a secret into this conversation or any MCP request. Tell them to choose one local setup path:
- Run `lua auth configure` after installing `lua-cli`; it writes `~/.lua-cli/credentials` and is the copy-paste-safe default for Cline.
- Set `LUA_API_KEY` in the MCP host's local environment.
- Add `LUA_API_KEY` to a local `.env` file in the MCP working directory.

The server selects sources in that order and forwards either legacy or dotted typed credentials unchanged. If the user needs to create or manage a credential, direct them to https://admin.heylua.ai rather than asking for the secret.

5. Restart Cline. Verify by asking: *"What lua-platform MCP tools are available?"* — expect 5 tools: `list_agents`, `get_agent`, `list_primitive_versions`, `get_deployment_status`, `tail_logs`.

Expand All @@ -54,7 +52,7 @@ Run from a Cline conversation:

## Troubleshooting

- **Server fails to start**: usually missing `LUA_API_KEY`. Set the env var or write `~/.lua-cli/credentials`.
- **Server fails to start**: usually no local credential source is configured. Set `LUA_API_KEY`, run `lua auth configure`, or add a local `.env` file.
- **`list_agents` returns empty**: the API key is valid but the user has no agents yet — direct them to admin.heylua.ai to create one.
- **HTTP 401 errors**: the key is invalid or expired. Regenerate at admin.heylua.ai.

Expand Down
10 changes: 10 additions & 0 deletions tests/api-client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ describe('apiRequest', () => {
expect(fetchFn.calls[0].init.headers.Authorization).toBe('Bearer lk_test_key');
});

test.each([
'api_0123456789abcdef0123456789abcdef',
'api_123e4567-e89b-12d3-a456-426614174000.abcdefghijklmnopqrstuvwxyz_0123456789-ABCDE',
])('forwards %s unchanged in the Authorization header', async (credential) => {
process.env.LUA_API_KEY = credential;
const fetchFn = mockFetch(jsonResponse({ ok: true }));
await apiRequest('/agents', { fetchFn });
expect(fetchFn.calls[0].init.headers.Authorization).toBe(`Bearer ${credential}`);
});

test('sets Content-Type: application/json', async () => {
const fetchFn = mockFetch(jsonResponse({ ok: true }));
await apiRequest('/agents', { fetchFn });
Expand Down
34 changes: 34 additions & 0 deletions tests/auth.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,40 @@ describe('Tier 3: .env file (regression test for iteration-8 fix)', () => {
});
});

describe('Credential format opacity', () => {
const legacyCredential = 'api_0123456789abcdef0123456789abcdef';
const typedCredential = 'api_123e4567-e89b-12d3-a456-426614174000.abcdefghijklmnopqrstuvwxyz_0123456789-ABCDE';

test.each([legacyCredential, typedCredential])('forwards %s unchanged from LUA_API_KEY', async (credential) => {
await expect(resolveApiKey({
env: { LUA_API_KEY: credential },
credentialsPath: join(tmpDir, 'nonexistent'),
cwd: tmpDir,
})).resolves.toBe(credential);
});

test.each([legacyCredential, typedCredential])(
'forwards %s unchanged from the lua-cli credentials file',
async (credential) => {
writeFileSync(join(tmpDir, 'credentials'), `${credential}\n`);
await expect(resolveApiKey({
env: {},
credentialsPath: join(tmpDir, 'credentials'),
cwd: tmpDir,
})).resolves.toBe(credential);
},
);

test.each([legacyCredential, typedCredential])('forwards %s unchanged from the working-directory .env', async (credential) => {
writeFileSync(join(tmpDir, '.env'), `LUA_API_KEY=${credential}\n`);
await expect(resolveApiKey({
env: {},
credentialsPath: join(tmpDir, 'nonexistent'),
cwd: tmpDir,
})).resolves.toBe(credential);
});
});

describe('No source resolves', () => {
test('throws MCP_AUTH_STALE with all three sources mentioned', async () => {
await expect(resolveApiKey({
Expand Down
Loading