pyxis-mcp 0.0.0 → 0.2.0
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/LICENSE +21 -21
- package/README.md +260 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/http.d.ts +12 -0
- package/dist/http.js +44 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/pyxis-api.d.ts +40 -0
- package/dist/pyxis-api.js +122 -0
- package/dist/pyxis-api.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +62 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +21 -0
- package/dist/tools.js +347 -0
- package/dist/tools.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/package.json +74 -4
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Samuel Santana
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samuel Santana
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,7 +1,262 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/samuelcsantana/pyxis-mcp/main/.github/assets/cover-dark.png">
|
|
5
|
+
<img alt="Pyxis — privacy-first product analytics" src="https://raw.githubusercontent.com/samuelcsantana/pyxis-mcp/main/.github/assets/cover-light.png" width="100%">
|
|
6
|
+
</picture>
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
**The MCP server of Pyxis: lets an AI agent read your dashboard's numbers, with a read-only token
|
|
9
|
+
you can revoke at any time.**
|
|
10
|
+
|
|
11
|
+
[](https://github.com/samuelcsantana/pyxis-mcp/actions/workflows/ci.yml)
|
|
12
|
+
[](https://codecov.io/gh/samuelcsantana/pyxis-mcp)
|
|
13
|
+
[](https://github.com/samuelcsantana/pyxis-mcp/actions/workflows/codeql.yml)
|
|
14
|
+
<br>
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
[](tsconfig.json)
|
|
17
|
+
[](https://www.conventionalcommits.org)
|
|
18
|
+
[](https://www.npmjs.com/package/pyxis-mcp)
|
|
19
|
+
[](https://www.npmjs.com/package/pyxis-mcp#provenance)
|
|
20
|
+
|
|
21
|
+
**[Pyxis](https://pyxis-analytics.dev)** · **[Live demo](https://demo.pyxis-analytics.dev)** ·
|
|
22
|
+
**[API reference](https://samuelcsantana.github.io/pyxis-api/)**
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
> **Status:** on npm as `pyxis-mcp`, every release published from GitHub Actions with
|
|
27
|
+
> provenance. The fifteen tools read the live Pyxis API. The package also exports them as a
|
|
28
|
+
> library, which a host serves over HTTP with each caller's token.
|
|
29
|
+
|
|
30
|
+
## Ecosystem
|
|
31
|
+
|
|
32
|
+
| Repository | Role |
|
|
33
|
+
| -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
34
|
+
| [pyxis-api](https://github.com/samuelcsantana/pyxis-api) | Ingestion, dashboard sign-in and queries, erasure, retention |
|
|
35
|
+
| [pyxis-sdk](https://github.com/samuelcsantana/pyxis-sdk) | Browser tracker published to npm as `pyxis-analytics` |
|
|
36
|
+
| [pyxis-web](https://github.com/samuelcsantana/pyxis-web) | The dashboard: overview, funnels, features, requests |
|
|
37
|
+
| **pyxis-mcp** (this one) | MCP server that lets an AI agent read the dashboard's numbers |
|
|
38
|
+
|
|
39
|
+
## Why it exists
|
|
40
|
+
|
|
41
|
+
More and more questions about a product are asked to an agent instead of a dashboard: "how did
|
|
42
|
+
last week compare", "where does the sign-up funnel lose people", "which route started failing".
|
|
43
|
+
This server gives Claude Code, Claude Desktop, Cursor and other MCP clients the same numbers the
|
|
44
|
+
Pyxis dashboard shows, and nothing more: it uses an access token that only reads, created by an
|
|
45
|
+
admin in the dashboard's Settings and revocable there at any moment. Like the rest of Pyxis, there
|
|
46
|
+
is no personal data to hand over: events carry no cookies, no addresses and no user agents.
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
Every tool only reads, and maps to one `GET` route of the Pyxis API. Dates are `YYYY-MM-DD` in the
|
|
51
|
+
project's time zone; `list_projects` gives each project's today, so call it first.
|
|
52
|
+
|
|
53
|
+
| Tool | What it answers |
|
|
54
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `list_projects` | The projects the token can read: id, name, time zone, today there, conversion event, first and last event |
|
|
56
|
+
| `overview` | Visits, identified users, conversions and failed writes against the previous period; top pages and events |
|
|
57
|
+
| `devices` | Visits and conversions by device type, browser, system, country and region |
|
|
58
|
+
| `acquisition` | Visits per day and channel, and the sources and campaigns behind them |
|
|
59
|
+
| `engagement` | Entry and exit pages, single-page visits, visit length |
|
|
60
|
+
| `time_of_day` | Visits by weekday and local hour |
|
|
61
|
+
| `features` | The most used named events or screens, day by day |
|
|
62
|
+
| `event_properties` | How the property values of one named event break down |
|
|
63
|
+
| `requests` | Calls to the site's backend per route: failures, statuses, durations, latest errors |
|
|
64
|
+
| `funnel` | How many visits or people reached each step, and the time between steps |
|
|
65
|
+
| `funnel_segments` | The same funnel per device type or channel |
|
|
66
|
+
| `funnel_subjects` | The visits or people who reached, or dropped at, one step |
|
|
67
|
+
| `visits` | The visits of a range, filtered by pages, events, channel, device, country, campaign, failed requests |
|
|
68
|
+
| `visit_timeline` | Every event of one visit, or every visit of one identified person |
|
|
69
|
+
| `project_settings` | Time zone, conversion event, allowed origins, retention, live public keys |
|
|
70
|
+
|
|
71
|
+
A result is JSON text, `{ "untrusted_visitor_text": "…", "report": … }`: the report is the API's
|
|
72
|
+
answer, and the first field reminds the agent that names, paths and values inside it were written by
|
|
73
|
+
the site's visitors. Problems come back as tool errors the agent can act on (a revoked token, an
|
|
74
|
+
unknown project, the rate limit and when to retry). When the API refuses a request (any `4xx`), the
|
|
75
|
+
error carries the API's own code and reason, such as a suspended account; only a `5xx` asks to try
|
|
76
|
+
again later.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
| Variable | Required | Meaning |
|
|
81
|
+
| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `PYXIS_TOKEN` | yes | A read-only access token (`pyxis_at_` and 32 letters and digits), created in the dashboard under Settings |
|
|
83
|
+
| `PYXIS_API_URL` | no | The Pyxis API to read, as an origin. Defaults to `https://api.pyxis-analytics.dev`; plain http only for localhost |
|
|
84
|
+
|
|
85
|
+
A missing or malformed value stops the server with a sentence on standard error that names the
|
|
86
|
+
variable and never repeats its value.
|
|
87
|
+
|
|
88
|
+
## Use it
|
|
89
|
+
|
|
90
|
+
Create a token in the dashboard (Settings, Access tokens), then add the server to your client. It
|
|
91
|
+
runs with `npx`, so there is nothing to install beforehand (Node 22 or later).
|
|
92
|
+
|
|
93
|
+
Claude Code:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add pyxis -e PYXIS_TOKEN=pyxis_at_… -- npx -y pyxis-mcp
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Claude Desktop (`claude_desktop_config.json`) and Cursor (`.cursor/mcp.json`):
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"pyxis": {
|
|
105
|
+
"command": "npx",
|
|
106
|
+
"args": ["-y", "pyxis-mcp"],
|
|
107
|
+
"env": { "PYXIS_TOKEN": "pyxis_at_…" }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A self-hosted Pyxis adds `PYXIS_API_URL`. To run it from source instead, `npm ci && npm run build`
|
|
114
|
+
in a clone and use `node /path/to/pyxis-mcp/dist/cli.js` as the command. Keep the token out of
|
|
115
|
+
shared files and chats; revoke it in the dashboard the moment it leaks.
|
|
116
|
+
|
|
117
|
+
## Use it as a library
|
|
118
|
+
|
|
119
|
+
Clients that only accept remote servers need the tools at a URL. The package exports them so a
|
|
120
|
+
host can serve them from its own endpoint, over Streamable HTTP, with the token of whoever is
|
|
121
|
+
calling. It is an ES module that a CommonJS host can also `require()` (Node 22.12 or later).
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { handleHttpRequest, PyxisApi } from 'pyxis-mcp';
|
|
125
|
+
|
|
126
|
+
export function answerMcp(request: Request, callerToken: string): Promise<Response> {
|
|
127
|
+
const api = new PyxisApi({
|
|
128
|
+
token: callerToken,
|
|
129
|
+
apiUrl: 'https://api.pyxis-analytics.dev',
|
|
130
|
+
problemTexts: {
|
|
131
|
+
refusedToken: 'Pyxis refused this connection: connect Pyxis again in your MCP client.',
|
|
132
|
+
unreachableAdvice: 'Try again in a moment.',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return handleHttpRequest(request, api);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| Export | What it does |
|
|
140
|
+
| ------------------------------------------- | --------------------------------------------------------------------- |
|
|
141
|
+
| `handleHttpRequest(request, api, options?)` | Answers one MCP exchange: a web `Request` in, a web `Response` out |
|
|
142
|
+
| `createServer(api, options?)` | The MCP server with the fifteen tools, for a transport of your choice |
|
|
143
|
+
| `new PyxisApi(options)` | The client the tools read through, one per caller |
|
|
144
|
+
|
|
145
|
+
`PyxisApi` options: `token` and `apiUrl` (required); `userAgent` (default `pyxis-mcp/<version>`);
|
|
146
|
+
`fetch` (default the global one; a host can pass one that answers inside its own process);
|
|
147
|
+
`timeoutMs` (default 15 seconds); `problemTexts`, the two sentences that depend on where the server
|
|
148
|
+
runs: `refusedToken` and `unreachableAdvice` (default: the local server's, which name
|
|
149
|
+
`PYXIS_TOKEN` and `PYXIS_API_URL`). `createServer` takes `version` (default the package's) and
|
|
150
|
+
`now` (the clock); `handleHttpRequest` takes those and `allowedOrigins`.
|
|
151
|
+
|
|
152
|
+
What the handler does: each request gets a server and a transport of its own, closed once it is
|
|
153
|
+
answered, so there is no session id and no event stream, only JSON answers. It answers `405` with
|
|
154
|
+
`Allow: POST` to any other method, `403` to a request with an `Origin` header unless that origin is
|
|
155
|
+
in `allowedOrigins` (none by default: MCP clients do not call from web pages), and `413` to a body
|
|
156
|
+
over 64 KiB. What it leaves to the host: authenticating the caller (and the `401` with
|
|
157
|
+
`WWW-Authenticate` that starts a client's sign-in), rate limits and the host name
|
|
158
|
+
([ADR 0003](docs/adr/0003-stateless-http-handler-for-hosts.md)).
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
```mermaid
|
|
163
|
+
flowchart LR
|
|
164
|
+
Agent["MCP client<br>(Claude Code, Cursor…)"] -- "stdio, JSON-RPC" --> Server["pyxis-mcp<br>(on the user's machine)"]
|
|
165
|
+
Server -- "HTTPS GET<br>Authorization: Bearer pyxis_at_…" --> API["Pyxis API"]
|
|
166
|
+
Remote["Remote MCP client"] -- "Streamable HTTP POST<br>with the caller's token" --> Host["A host's endpoint<br>handleHttpRequest"]
|
|
167
|
+
Host -- "GET with the same token" --> API
|
|
168
|
+
API --> DB[("Events")]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The local server opens no port and keeps no state. The API decides what the token may read,
|
|
172
|
+
exactly as it does for a dashboard session ([ADR 0002](docs/adr/0002-local-read-only-server.md)).
|
|
173
|
+
A host runs the same tools for remote clients: it authenticates the caller, and the tools read with
|
|
174
|
+
that caller's token, so the token never leaves the server that issued it
|
|
175
|
+
([ADR 0003](docs/adr/0003-stateless-http-handler-for-hosts.md)).
|
|
176
|
+
|
|
177
|
+
## Tech stack
|
|
178
|
+
|
|
179
|
+
TypeScript (strict) on Node 22 or later, ES modules. Vitest with V8 coverage, ESLint
|
|
180
|
+
(`typescript-eslint` strict, type-checked) and Prettier, Husky and commitlint, GitHub Actions with
|
|
181
|
+
CodeQL, Dependabot and release-please.
|
|
182
|
+
|
|
183
|
+
## Getting started
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm ci
|
|
187
|
+
npm run lint && npm run typecheck && npm run test:cov && npm run build
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Testing
|
|
191
|
+
|
|
192
|
+
- `npm run test:cov` runs the unit tests in Node and fails below **100%** of statements, branches,
|
|
193
|
+
functions and lines. The server is tested with a real MCP client over an in-memory transport and
|
|
194
|
+
a fake `fetch`; the HTTP handler with web `Request` objects, from `initialize` to a tool call,
|
|
195
|
+
and every refusal.
|
|
196
|
+
- One file is left out of the unit coverage: `src/cli.ts`, which only wires the modules to the
|
|
197
|
+
standard input and output. `npm run test:e2e` covers it: after `npm run build`, a real MCP client
|
|
198
|
+
starts `dist/cli.js`, lists the tools and calls them against a local fake of the API, checks that
|
|
199
|
+
standard output carries nothing but the protocol, and that a bad configuration stops the process
|
|
200
|
+
with the reason on standard error.
|
|
201
|
+
- `npm run test:e2e` also starts `e2e/http-host.ts`, a small HTTP host that imports `pyxis-mcp` by
|
|
202
|
+
its name, and drives it with the SDK's Streamable HTTP client: tools against the fake API with
|
|
203
|
+
the caller's token, no session, the host's own words for a refused token, `405` and `403`. The
|
|
204
|
+
package is also loaded by name with `import` and with `require`.
|
|
205
|
+
- `src/contract.test.ts` checks the tools against `contract/openapi.json`, the API's own contract
|
|
206
|
+
(`npm run contract:sync` copies it from pyxis-api): every route a token may read has a tool, and
|
|
207
|
+
every tool sends only declared parameters, with values inside their enums.
|
|
208
|
+
- `npm run test:tooling` tests the lint rule and the scripts.
|
|
209
|
+
- CI runs the unit tests on Node 24 and again on Node 22, the oldest version the package supports.
|
|
210
|
+
|
|
211
|
+
## Project structure
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
src/
|
|
215
|
+
├── config.ts reads PYXIS_TOKEN and PYXIS_API_URL
|
|
216
|
+
├── pyxis-api.ts one GET to the API, failures as sentences
|
|
217
|
+
├── tools.ts the fifteen tools: input schemas, routes, requests
|
|
218
|
+
├── server.ts registers the tools on the MCP server
|
|
219
|
+
├── http.ts one stateless Streamable HTTP exchange
|
|
220
|
+
├── index.ts the library's public exports
|
|
221
|
+
├── version.ts the package version, for the server and the user agent
|
|
222
|
+
└── cli.ts the executable: stdio transport
|
|
223
|
+
e2e/ the built package driven by real MCP clients, over stdio and over HTTP
|
|
224
|
+
contract/ the API's OpenAPI contract, copied from pyxis-api
|
|
225
|
+
eslint-rules/ the local rule that forbids comments
|
|
226
|
+
scripts/ the comment check and the contract sync
|
|
227
|
+
docs/adr/ architecture decision records
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Privacy and security
|
|
231
|
+
|
|
232
|
+
- Read-only: the API accepts the token only on its read routes; it cannot write, sign in, manage
|
|
233
|
+
sessions or create tokens.
|
|
234
|
+
- The token only travels in the `Authorization` header to the configured API, over https (plain
|
|
235
|
+
http only to localhost), and never reaches a log, an error message or a tool result.
|
|
236
|
+
- Text written by a site's visitors is returned as data, and each tool description says it is not
|
|
237
|
+
an instruction.
|
|
238
|
+
- Over HTTP, the handler keeps no session, refuses requests sent by web pages and bodies over 64
|
|
239
|
+
KiB, and reads with the token of the caller its host authenticated.
|
|
240
|
+
- Vulnerabilities: see [SECURITY.md](SECURITY.md).
|
|
241
|
+
|
|
242
|
+
## Architecture decisions
|
|
243
|
+
|
|
244
|
+
| ADR | Decision |
|
|
245
|
+
| --------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
246
|
+
| [0001](docs/adr/0001-record-architecture-decisions.md) | Record architecture decisions |
|
|
247
|
+
| [0002](docs/adr/0002-local-read-only-server.md) | A local, read-only server that uses the admin's access token |
|
|
248
|
+
| [0003](docs/adr/0003-stateless-http-handler-for-hosts.md) | A stateless HTTP handler that a host runs with the caller's token |
|
|
249
|
+
|
|
250
|
+
## Roadmap
|
|
251
|
+
|
|
252
|
+
- [x] Repository skeleton, quality gates, configuration reader
|
|
253
|
+
- [x] Tools over the API's read routes, tested against its OpenAPI contract
|
|
254
|
+
- [x] First release on npm, with provenance
|
|
255
|
+
- [x] The tools as a library, served over stateless Streamable HTTP by a host
|
|
256
|
+
- [ ] A remote endpoint in the Pyxis API, signed in with OAuth, for clients that only take URLs
|
|
257
|
+
- [ ] Setup guides for Claude Code, Claude Desktop and Cursor
|
|
258
|
+
|
|
259
|
+
## Contributing and license
|
|
260
|
+
|
|
261
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). MIT licensed:
|
|
262
|
+
see [LICENSE](LICENSE).
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { ConfigError, readConfig } from './config.js';
|
|
4
|
+
import { PyxisApi } from './pyxis-api.js';
|
|
5
|
+
import { createServer } from './server.js';
|
|
6
|
+
const EXIT_FAILURE = 1;
|
|
7
|
+
async function start() {
|
|
8
|
+
const server = createServer(new PyxisApi(readConfig(process.env)));
|
|
9
|
+
await server.connect(new StdioServerTransport());
|
|
10
|
+
}
|
|
11
|
+
start().catch((error) => {
|
|
12
|
+
const reason = error instanceof ConfigError
|
|
13
|
+
? error.message
|
|
14
|
+
: `could not start: ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`;
|
|
15
|
+
process.stderr.write(`pyxis-mcp: ${reason}\n`);
|
|
16
|
+
process.exitCode = EXIT_FAILURE;
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,KAAK,UAAU,KAAK;IAClB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC/B,MAAM,MAAM,GACV,KAAK,YAAY,WAAW;QAC1B,CAAC,CAAC,KAAK,CAAC,OAAO;QACf,CAAC,CAAC,oBAAoB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACvG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,MAAM,IAAI,CAAC,CAAC;IAC/C,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC;AAClC,CAAC,CAAC,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_API_URL = "https://api.pyxis-analytics.dev";
|
|
2
|
+
export declare const ACCESS_TOKEN_PATTERN: RegExp;
|
|
3
|
+
export interface PyxisConfig {
|
|
4
|
+
readonly token: string;
|
|
5
|
+
readonly apiUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export type ConfigProblem = 'missing-token' | 'malformed-token' | 'invalid-api-url' | 'insecure-api-url';
|
|
8
|
+
export declare class ConfigError extends Error {
|
|
9
|
+
readonly problem: ConfigProblem;
|
|
10
|
+
constructor(problem: ConfigProblem);
|
|
11
|
+
}
|
|
12
|
+
export type Environment = Readonly<Record<string, string | undefined>>;
|
|
13
|
+
export declare function readConfig(environment: Environment): PyxisConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const DEFAULT_API_URL = 'https://api.pyxis-analytics.dev';
|
|
2
|
+
export const ACCESS_TOKEN_PATTERN = /^pyxis_at_[A-Za-z0-9]{32}$/;
|
|
3
|
+
const SECURE_PROTOCOL = 'https:';
|
|
4
|
+
const LOCAL_PROTOCOL = 'http:';
|
|
5
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
6
|
+
const ROOT_PATH = '/';
|
|
7
|
+
const PROBLEM_MESSAGES = {
|
|
8
|
+
'missing-token': 'Set PYXIS_TOKEN to a read-only access token created in the Pyxis dashboard, under Settings, Access tokens.',
|
|
9
|
+
'malformed-token': 'PYXIS_TOKEN is not a Pyxis access token: it starts with pyxis_at_ followed by 32 letters and digits.',
|
|
10
|
+
'invalid-api-url': 'PYXIS_API_URL must be the address of a Pyxis API with no path, such as https://api.pyxis-analytics.dev.',
|
|
11
|
+
'insecure-api-url': 'PYXIS_API_URL must use https; plain http is accepted only for localhost.',
|
|
12
|
+
};
|
|
13
|
+
export class ConfigError extends Error {
|
|
14
|
+
problem;
|
|
15
|
+
constructor(problem) {
|
|
16
|
+
super(PROBLEM_MESSAGES[problem]);
|
|
17
|
+
this.problem = problem;
|
|
18
|
+
this.name = 'ConfigError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function readConfig(environment) {
|
|
22
|
+
return {
|
|
23
|
+
token: readToken(environment.PYXIS_TOKEN),
|
|
24
|
+
apiUrl: readApiUrl(environment.PYXIS_API_URL),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function readToken(raw) {
|
|
28
|
+
const token = raw?.trim() ?? '';
|
|
29
|
+
if (token === '') {
|
|
30
|
+
throw new ConfigError('missing-token');
|
|
31
|
+
}
|
|
32
|
+
if (!ACCESS_TOKEN_PATTERN.test(token)) {
|
|
33
|
+
throw new ConfigError('malformed-token');
|
|
34
|
+
}
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
function isBareOrigin(url) {
|
|
38
|
+
return (url.username === '' &&
|
|
39
|
+
url.password === '' &&
|
|
40
|
+
url.pathname === ROOT_PATH &&
|
|
41
|
+
url.search === '' &&
|
|
42
|
+
url.hash === '');
|
|
43
|
+
}
|
|
44
|
+
function readApiUrl(raw) {
|
|
45
|
+
const value = raw?.trim() ?? '';
|
|
46
|
+
if (value === '') {
|
|
47
|
+
return DEFAULT_API_URL;
|
|
48
|
+
}
|
|
49
|
+
const url = URL.parse(value);
|
|
50
|
+
if (url === null || !isBareOrigin(url)) {
|
|
51
|
+
throw new ConfigError('invalid-api-url');
|
|
52
|
+
}
|
|
53
|
+
if (url.protocol === SECURE_PROTOCOL) {
|
|
54
|
+
return url.origin;
|
|
55
|
+
}
|
|
56
|
+
if (url.protocol !== LOCAL_PROTOCOL) {
|
|
57
|
+
throw new ConfigError('invalid-api-url');
|
|
58
|
+
}
|
|
59
|
+
if (!LOCAL_HOSTS.has(url.hostname)) {
|
|
60
|
+
throw new ConfigError('insecure-api-url');
|
|
61
|
+
}
|
|
62
|
+
return url.origin;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iCAAiC,CAAC;AACjE,MAAM,CAAC,MAAM,oBAAoB,GAAG,4BAA4B,CAAC;AAEjE,MAAM,eAAe,GAAG,QAAQ,CAAC;AACjC,MAAM,cAAc,GAAG,OAAO,CAAC;AAC/B,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AACtF,MAAM,SAAS,GAAG,GAAG,CAAC;AAUtB,MAAM,gBAAgB,GAA4C;IAChE,eAAe,EACb,4GAA4G;IAC9G,iBAAiB,EACf,sGAAsG;IACxG,iBAAiB,EACf,yGAAyG;IAC3G,kBAAkB,EAAE,0EAA0E;CAC/F,CAAC;AAEF,MAAM,OAAO,WAAY,SAAQ,KAAK;IACf;IAArB,YAAqB,OAAsB;QACzC,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;QADd,YAAO,GAAP,OAAO,CAAe;QAEzC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAID,MAAM,UAAU,UAAU,CAAC,WAAwB;IACjD,OAAO;QACL,KAAK,EAAE,SAAS,CAAC,WAAW,CAAC,WAAW,CAAC;QACzC,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,aAAa,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,GAAuB;IACxC,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAChC,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,WAAW,CAAC,eAAe,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,GAAQ;IAC5B,OAAO,CACL,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,SAAS;QAC1B,GAAG,CAAC,MAAM,KAAK,EAAE;QACjB,GAAG,CAAC,IAAI,KAAK,EAAE,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAuB;IACzC,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAChC,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,eAAe,EAAE,CAAC;QACrC,OAAO,GAAG,CAAC,MAAM,CAAC;IACpB,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,cAAc,EAAE,CAAC;QACpC,MAAM,IAAI,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC;AACpB,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PyxisApi } from './pyxis-api.js';
|
|
2
|
+
import { type ServerOptions } from './server.js';
|
|
3
|
+
export declare const MAX_REQUEST_BYTES: number;
|
|
4
|
+
export declare const ALLOWED_METHOD = "POST";
|
|
5
|
+
export declare const HTTP_REFUSALS: {
|
|
6
|
+
readonly method: "Method not allowed: this server keeps no session and answers POST only.";
|
|
7
|
+
readonly origin: "Forbidden: this server does not answer requests sent by a web page.";
|
|
8
|
+
};
|
|
9
|
+
export interface HttpOptions extends ServerOptions {
|
|
10
|
+
readonly allowedOrigins?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function handleHttpRequest(request: Request, api: PyxisApi, options?: HttpOptions): Promise<Response>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
2
|
+
import { createServer } from './server.js';
|
|
3
|
+
export const MAX_REQUEST_BYTES = 64 * 1024;
|
|
4
|
+
export const ALLOWED_METHOD = 'POST';
|
|
5
|
+
export const HTTP_REFUSALS = {
|
|
6
|
+
method: 'Method not allowed: this server keeps no session and answers POST only.',
|
|
7
|
+
origin: 'Forbidden: this server does not answer requests sent by a web page.',
|
|
8
|
+
};
|
|
9
|
+
const STATUS_FORBIDDEN = 403;
|
|
10
|
+
const STATUS_METHOD_NOT_ALLOWED = 405;
|
|
11
|
+
const JSON_RPC_SERVER_ERROR = -32000;
|
|
12
|
+
const JSON_MEDIA_TYPE = 'application/json';
|
|
13
|
+
function refusal(status, message, headers = {}) {
|
|
14
|
+
return new Response(JSON.stringify({
|
|
15
|
+
jsonrpc: '2.0',
|
|
16
|
+
error: { code: JSON_RPC_SERVER_ERROR, message },
|
|
17
|
+
id: null,
|
|
18
|
+
}), { status, headers: { 'content-type': JSON_MEDIA_TYPE, ...headers } });
|
|
19
|
+
}
|
|
20
|
+
function comesFromForeignPage(request, allowedOrigins) {
|
|
21
|
+
const origin = request.headers.get('origin');
|
|
22
|
+
return origin !== null && !allowedOrigins.includes(origin);
|
|
23
|
+
}
|
|
24
|
+
export async function handleHttpRequest(request, api, options = {}) {
|
|
25
|
+
if (request.method !== ALLOWED_METHOD) {
|
|
26
|
+
return refusal(STATUS_METHOD_NOT_ALLOWED, HTTP_REFUSALS.method, { allow: ALLOWED_METHOD });
|
|
27
|
+
}
|
|
28
|
+
if (comesFromForeignPage(request, options.allowedOrigins ?? [])) {
|
|
29
|
+
return refusal(STATUS_FORBIDDEN, HTTP_REFUSALS.origin);
|
|
30
|
+
}
|
|
31
|
+
const server = createServer(api, options);
|
|
32
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
33
|
+
enableJsonResponse: true,
|
|
34
|
+
maxRequestBodySize: MAX_REQUEST_BYTES,
|
|
35
|
+
});
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
try {
|
|
38
|
+
return await transport.handleRequest(request);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await server.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wCAAwC,EAAE,MAAM,+DAA+D,CAAC;AAEzH,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAE/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC;AAC3C,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AAErC,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,MAAM,EAAE,yEAAyE;IACjF,MAAM,EAAE,qEAAqE;CACrE,CAAC;AAEX,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,yBAAyB,GAAG,GAAG,CAAC;AACtC,MAAM,qBAAqB,GAAG,CAAC,KAAK,CAAC;AACrC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAM3C,SAAS,OAAO,CAAC,MAAc,EAAE,OAAe,EAAE,UAAkC,EAAE;IACpF,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC;QACb,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE;QAC/C,EAAE,EAAE,IAAI;KACT,CAAC,EACF,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,eAAe,EAAE,GAAG,OAAO,EAAE,EAAE,CACrE,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,OAAgB,EAAE,cAAiC;IAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7C,OAAO,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAgB,EAChB,GAAa,EACb,UAAuB,EAAE;IAEzB,IAAI,OAAO,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QACtC,OAAO,OAAO,CAAC,yBAAyB,EAAE,aAAa,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,IAAI,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC,EAAE,CAAC;QAChE,OAAO,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAG,IAAI,wCAAwC,CAAC;QAC7D,kBAAkB,EAAE,IAAI;QACxB,kBAAkB,EAAE,iBAAiB;KACtC,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAC;AAChE,OAAO,EACL,QAAQ,GAOT,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PyxisConfig } from './config.js';
|
|
2
|
+
export declare const REQUEST_TIMEOUT_MS = 15000;
|
|
3
|
+
export declare const REQUESTS_PER_MINUTE = 60;
|
|
4
|
+
export type QueryValue = string | number | boolean | readonly string[] | undefined;
|
|
5
|
+
export type Query = Readonly<Record<string, QueryValue>>;
|
|
6
|
+
export type ApiOutcome = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly body: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly problem: string;
|
|
12
|
+
};
|
|
13
|
+
export type Fetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
14
|
+
export interface ProblemTexts {
|
|
15
|
+
readonly refusedToken: string;
|
|
16
|
+
readonly unreachableAdvice: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const LOCAL_PROBLEM_TEXTS: ProblemTexts;
|
|
19
|
+
export interface PyxisApiOptions extends PyxisConfig {
|
|
20
|
+
readonly userAgent?: string;
|
|
21
|
+
readonly fetch?: Fetch;
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
readonly problemTexts?: ProblemTexts;
|
|
24
|
+
}
|
|
25
|
+
export declare const PROBLEMS: {
|
|
26
|
+
readonly notFound: "Not found: no project with this id among the projects this token can read. Call list_projects for the ids.";
|
|
27
|
+
readonly rejected: "The Pyxis API refused the request.";
|
|
28
|
+
readonly notJson: "The Pyxis API answered with something that is not JSON.";
|
|
29
|
+
};
|
|
30
|
+
export declare function requestUrl(apiUrl: string, path: string, query: Query): string;
|
|
31
|
+
export declare class PyxisApi {
|
|
32
|
+
private readonly token;
|
|
33
|
+
private readonly apiUrl;
|
|
34
|
+
private readonly userAgent;
|
|
35
|
+
private readonly fetch;
|
|
36
|
+
private readonly timeoutMs;
|
|
37
|
+
private readonly problemTexts;
|
|
38
|
+
constructor(options: PyxisApiOptions);
|
|
39
|
+
get(path: string, query?: Query): Promise<ApiOutcome>;
|
|
40
|
+
}
|