pyxis-mcp 0.1.0 → 0.3.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/README.md CHANGED
@@ -5,8 +5,8 @@
5
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
6
  </picture>
7
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.**
8
+ **The MCP server of Pyxis: lets Claude and other AI agents read your dashboard's numbers, only
9
+ reading, through a connection or a token you can revoke at any time.**
10
10
 
11
11
  [![CI](https://github.com/samuelcsantana/pyxis-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/samuelcsantana/pyxis-mcp/actions/workflows/ci.yml)
12
12
  [![Coverage](https://codecov.io/gh/samuelcsantana/pyxis-mcp/graph/badge.svg)](https://codecov.io/gh/samuelcsantana/pyxis-mcp)
@@ -15,6 +15,8 @@ you can revoke at any time.**
15
15
  [![License: MIT](https://img.shields.io/github/license/samuelcsantana/pyxis-mcp)](LICENSE)
16
16
  [![TypeScript strict](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](tsconfig.json)
17
17
  [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-FE5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org)
18
+ [![npm](https://img.shields.io/npm/v/pyxis-mcp?logo=npm)](https://www.npmjs.com/package/pyxis-mcp)
19
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-2E7D32?logo=npm)](https://www.npmjs.com/package/pyxis-mcp#provenance)
18
20
 
19
21
  **[Pyxis](https://pyxis-analytics.dev)** · **[Live demo](https://demo.pyxis-analytics.dev)** ·
20
22
  **[API reference](https://samuelcsantana.github.io/pyxis-api/)**
@@ -22,7 +24,9 @@ you can revoke at any time.**
22
24
  </div>
23
25
 
24
26
  > **Status:** on npm as `pyxis-mcp`, every release published from GitHub Actions with
25
- > provenance. The fifteen tools read the live Pyxis API.
27
+ > provenance. The fifteen tools read the live Pyxis API. The package also exports them as a
28
+ > library, and the Pyxis API serves them at `https://api.pyxis-analytics.dev/mcp`, the remote
29
+ > connector that Claude and Claude Code sign in to with OAuth.
26
30
 
27
31
  ## Ecosystem
28
32
 
@@ -45,7 +49,10 @@ is no personal data to hand over: events carry no cookies, no addresses and no u
45
49
  ## Tools
46
50
 
47
51
  Every tool only reads, and maps to one `GET` route of the Pyxis API. Dates are `YYYY-MM-DD` in the
48
- project's time zone; `list_projects` gives each project's today, so call it first.
52
+ project's time zone; `list_projects` gives each project's today, so call it first. The server's
53
+ instructions tell the agent what an empty account means: with no project, one is created in the
54
+ dashboard; with a project that has received no event, the site does not run the tracker yet, and
55
+ [llms.txt](https://pyxis-analytics.dev/llms.txt) explains how to install it.
49
56
 
50
57
  | Tool | What it answers |
51
58
  | ------------------ | --------------------------------------------------------------------------------------------------------- |
@@ -68,7 +75,43 @@ project's time zone; `list_projects` gives each project's today, so call it firs
68
75
  A result is JSON text, `{ "untrusted_visitor_text": "…", "report": … }`: the report is the API's
69
76
  answer, and the first field reminds the agent that names, paths and values inside it were written by
70
77
  the site's visitors. Problems come back as tool errors the agent can act on (a revoked token, an
71
- unknown project, the rate limit and when to retry).
78
+ unknown project, the rate limit and when to retry). When the API refuses a request (any `4xx`), the
79
+ error carries the API's own code and reason, such as a suspended account; only a `5xx` asks to try
80
+ again later.
81
+
82
+ ## Connect from Claude
83
+
84
+ The hosted Pyxis serves these tools at **`https://api.pyxis-analytics.dev/mcp`**. Claude connects
85
+ to it with no token and nothing to install: you sign in to Pyxis once and allow the connection.
86
+
87
+ **Claude** (on the web, the desktop app and the phone):
88
+
89
+ 1. In Claude, open Settings, then Connectors, and choose **Add custom connector**. In some versions
90
+ the menu is Customize, then Connectors.
91
+ 2. Name it `Pyxis` and paste `https://api.pyxis-analytics.dev/mcp`. Leave the advanced OAuth fields
92
+ empty.
93
+ 3. Choose **Connect**. A Pyxis page opens: sign in with the code sent to your e-mail, check the
94
+ app, where its data goes and your projects, and choose **Allow**.
95
+
96
+ **Claude Code:**
97
+
98
+ ```bash
99
+ claude mcp add --transport http pyxis https://api.pyxis-analytics.dev/mcp
100
+ ```
101
+
102
+ Then run `/mcp` in Claude Code, pick `pyxis` and choose **Authenticate**: the same Pyxis page
103
+ opens in the browser.
104
+
105
+ A connection only reads the numbers you see in the dashboard, from every project you can open. It
106
+ renews itself while it is used (each access token lasts an hour) and ends when you revoke it in the
107
+ dashboard, under Settings, Connected apps, or after 90 days without use; you keep up to ten. The
108
+ data Claude asks for goes from the Pyxis API to Anthropic, which handles it under its own terms.
109
+ Only Claude and Claude Code connect this way. A client that sends a header reaches the same address
110
+ with an access token instead:
111
+
112
+ ```bash
113
+ claude mcp add --transport http pyxis https://api.pyxis-analytics.dev/mcp --header "Authorization: Bearer pyxis_at_…"
114
+ ```
72
115
 
73
116
  ## Configuration
74
117
 
@@ -80,10 +123,11 @@ unknown project, the rate limit and when to retry).
80
123
  A missing or malformed value stops the server with a sentence on standard error that names the
81
124
  variable and never repeats its value.
82
125
 
83
- ## Use it
126
+ ## Run it on your computer
84
127
 
85
- Create a token in the dashboard (Settings, Access tokens), then add the server to your client. It
86
- runs with `npx`, so there is nothing to install beforehand (Node 22 or later).
128
+ To run the tools on your own computer, or against a self-hosted Pyxis, create a token in the
129
+ dashboard (Settings, Access tokens), then add the server to your client. It runs with `npx`, so
130
+ there is nothing to install beforehand (Node 22 or later).
87
131
 
88
132
  Claude Code:
89
133
 
@@ -109,17 +153,66 @@ A self-hosted Pyxis adds `PYXIS_API_URL`. To run it from source instead, `npm ci
109
153
  in a clone and use `node /path/to/pyxis-mcp/dist/cli.js` as the command. Keep the token out of
110
154
  shared files and chats; revoke it in the dashboard the moment it leaks.
111
155
 
156
+ ## Use it as a library
157
+
158
+ Clients that only accept remote servers need the tools at a URL. The package exports them so a
159
+ host can serve them from its own endpoint, over Streamable HTTP, with the token of whoever is
160
+ calling. It is an ES module that a CommonJS host can also `require()` (Node 22.12 or later).
161
+
162
+ ```ts
163
+ import { handleHttpRequest, PyxisApi } from 'pyxis-mcp';
164
+
165
+ export function answerMcp(request: Request, callerToken: string): Promise<Response> {
166
+ const api = new PyxisApi({
167
+ token: callerToken,
168
+ apiUrl: 'https://api.pyxis-analytics.dev',
169
+ problemTexts: {
170
+ refusedToken: 'Pyxis refused this connection: connect Pyxis again in your MCP client.',
171
+ unreachableAdvice: 'Try again in a moment.',
172
+ },
173
+ });
174
+ return handleHttpRequest(request, api);
175
+ }
176
+ ```
177
+
178
+ | Export | What it does |
179
+ | ------------------------------------------- | --------------------------------------------------------------------- |
180
+ | `handleHttpRequest(request, api, options?)` | Answers one MCP exchange: a web `Request` in, a web `Response` out |
181
+ | `createServer(api, options?)` | The MCP server with the fifteen tools, for a transport of your choice |
182
+ | `new PyxisApi(options)` | The client the tools read through, one per caller |
183
+
184
+ `PyxisApi` options: `token` and `apiUrl` (required); `userAgent` (default `pyxis-mcp/<version>`);
185
+ `fetch` (default the global one; a host can pass one that answers inside its own process);
186
+ `timeoutMs` (default 15 seconds); `problemTexts`, the two sentences that depend on where the server
187
+ runs: `refusedToken` and `unreachableAdvice` (default: the local server's, which name
188
+ `PYXIS_TOKEN` and `PYXIS_API_URL`). `createServer` takes `version` (default the package's) and
189
+ `now` (the clock); `handleHttpRequest` takes those and `allowedOrigins`.
190
+
191
+ What the handler does: each request gets a server and a transport of its own, closed once it is
192
+ answered, so there is no session id and no event stream, only JSON answers. It answers `405` with
193
+ `Allow: POST` to any other method, `403` to a request with an `Origin` header unless that origin is
194
+ in `allowedOrigins` (none by default: MCP clients do not call from web pages), and `413` to a body
195
+ over 64 KiB. What it leaves to the host: authenticating the caller (and the `401` with
196
+ `WWW-Authenticate` that starts a client's sign-in), rate limits and the host name
197
+ ([ADR 0003](docs/adr/0003-stateless-http-handler-for-hosts.md)).
198
+
112
199
  ## Architecture
113
200
 
114
201
  ```mermaid
115
202
  flowchart LR
116
203
  Agent["MCP client<br>(Claude Code, Cursor…)"] -- "stdio, JSON-RPC" --> Server["pyxis-mcp<br>(on the user's machine)"]
117
204
  Server -- "HTTPS GET<br>Authorization: Bearer pyxis_at_…" --> API["Pyxis API"]
205
+ Remote["Claude · Claude Code"] -- "Streamable HTTP POST<br>OAuth access token" --> Host["Pyxis API, /mcp<br>handleHttpRequest"]
206
+ Host -- "GET in process, same token" --> API
118
207
  API --> DB[("Events")]
119
208
  ```
120
209
 
121
- The server opens no port and keeps no state. The API decides what the token may read, exactly as
122
- it does for a dashboard session ([ADR 0002](docs/adr/0002-local-read-only-server.md)).
210
+ The local server opens no port and keeps no state. The API decides what the token may read,
211
+ exactly as it does for a dashboard session ([ADR 0002](docs/adr/0002-local-read-only-server.md)).
212
+ A host runs the same tools for remote clients: it authenticates the caller, and the tools read with
213
+ that caller's token, so the token never leaves the server that issued it
214
+ ([ADR 0003](docs/adr/0003-stateless-http-handler-for-hosts.md)). The Pyxis API is that host: it
215
+ signs Claude in with OAuth and runs the tools at `/mcp`.
123
216
 
124
217
  ## Tech stack
125
218
 
@@ -138,12 +231,17 @@ npm run lint && npm run typecheck && npm run test:cov && npm run build
138
231
 
139
232
  - `npm run test:cov` runs the unit tests in Node and fails below **100%** of statements, branches,
140
233
  functions and lines. The server is tested with a real MCP client over an in-memory transport and
141
- a fake `fetch`.
234
+ a fake `fetch`; the HTTP handler with web `Request` objects, from `initialize` to a tool call,
235
+ and every refusal.
142
236
  - One file is left out of the unit coverage: `src/cli.ts`, which only wires the modules to the
143
237
  standard input and output. `npm run test:e2e` covers it: after `npm run build`, a real MCP client
144
238
  starts `dist/cli.js`, lists the tools and calls them against a local fake of the API, checks that
145
239
  standard output carries nothing but the protocol, and that a bad configuration stops the process
146
240
  with the reason on standard error.
241
+ - `npm run test:e2e` also starts `e2e/http-host.ts`, a small HTTP host that imports `pyxis-mcp` by
242
+ its name, and drives it with the SDK's Streamable HTTP client: tools against the fake API with
243
+ the caller's token, no session, the host's own words for a refused token, `405` and `403`. The
244
+ package is also loaded by name with `import` and with `require`.
147
245
  - `src/contract.test.ts` checks the tools against `contract/openapi.json`, the API's own contract
148
246
  (`npm run contract:sync` copies it from pyxis-api): every route a token may read has a tool, and
149
247
  every tool sends only declared parameters, with values inside their enums.
@@ -158,9 +256,11 @@ src/
158
256
  ├── pyxis-api.ts one GET to the API, failures as sentences
159
257
  ├── tools.ts the fifteen tools: input schemas, routes, requests
160
258
  ├── server.ts registers the tools on the MCP server
259
+ ├── http.ts one stateless Streamable HTTP exchange
260
+ ├── index.ts the library's public exports
161
261
  ├── version.ts the package version, for the server and the user agent
162
262
  └── cli.ts the executable: stdio transport
163
- e2e/ the built server driven by a real MCP client
263
+ e2e/ the built package driven by real MCP clients, over stdio and over HTTP
164
264
  contract/ the API's OpenAPI contract, copied from pyxis-api
165
265
  eslint-rules/ the local rule that forbids comments
166
266
  scripts/ the comment check and the contract sync
@@ -175,21 +275,26 @@ docs/adr/ architecture decision records
175
275
  http only to localhost), and never reaches a log, an error message or a tool result.
176
276
  - Text written by a site's visitors is returned as data, and each tool description says it is not
177
277
  an instruction.
278
+ - Over HTTP, the handler keeps no session, refuses requests sent by web pages and bodies over 64
279
+ KiB, and reads with the token of the caller its host authenticated.
178
280
  - Vulnerabilities: see [SECURITY.md](SECURITY.md).
179
281
 
180
282
  ## Architecture decisions
181
283
 
182
- | ADR | Decision |
183
- | ------------------------------------------------------ | ------------------------------------------------------------ |
184
- | [0001](docs/adr/0001-record-architecture-decisions.md) | Record architecture decisions |
185
- | [0002](docs/adr/0002-local-read-only-server.md) | A local, read-only server that uses the admin's access token |
284
+ | ADR | Decision |
285
+ | --------------------------------------------------------- | ----------------------------------------------------------------- |
286
+ | [0001](docs/adr/0001-record-architecture-decisions.md) | Record architecture decisions |
287
+ | [0002](docs/adr/0002-local-read-only-server.md) | A local, read-only server that uses the admin's access token |
288
+ | [0003](docs/adr/0003-stateless-http-handler-for-hosts.md) | A stateless HTTP handler that a host runs with the caller's token |
186
289
 
187
290
  ## Roadmap
188
291
 
189
292
  - [x] Repository skeleton, quality gates, configuration reader
190
293
  - [x] Tools over the API's read routes, tested against its OpenAPI contract
191
294
  - [x] First release on npm, with provenance
192
- - [ ] Setup guides for Claude Code, Claude Desktop and Cursor
295
+ - [x] The tools as a library, served over stateless Streamable HTTP by a host
296
+ - [x] A remote endpoint in the Pyxis API, signed in with OAuth, for clients that only take URLs
297
+ - [x] Setup for Claude, Claude Code and clients that send a header
193
298
 
194
299
  ## Contributing and license
195
300
 
package/dist/cli.js CHANGED
@@ -3,11 +3,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { ConfigError, readConfig } from './config.js';
4
4
  import { PyxisApi } from './pyxis-api.js';
5
5
  import { createServer } from './server.js';
6
- import { PACKAGE_VERSION, userAgent } from './version.js';
7
6
  const EXIT_FAILURE = 1;
8
7
  async function start() {
9
- const config = readConfig(process.env);
10
- const server = createServer(new PyxisApi(config, userAgent()), { version: PACKAGE_VERSION });
8
+ const server = createServer(new PyxisApi(readConfig(process.env)));
11
9
  await server.connect(new StdioServerTransport());
12
10
  }
13
11
  start().catch((error) => {
package/dist/cli.js.map CHANGED
@@ -1 +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;AAC3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1D,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,KAAK,UAAU,KAAK;IAClB,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IAC7F,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"}
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/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
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export { handleHttpRequest, type HttpOptions } from './http.js';
2
+ export { PyxisApi, type ApiOutcome, type Fetch, type ProblemTexts, type PyxisApiOptions, type Query, type QueryValue, } from './pyxis-api.js';
3
+ export { createServer, type ServerOptions } from './server.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { handleHttpRequest } from './http.js';
2
+ export { PyxisApi, } from './pyxis-api.js';
3
+ export { createServer } from './server.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -11,18 +11,30 @@ export type ApiOutcome = {
11
11
  readonly problem: string;
12
12
  };
13
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
+ }
14
25
  export declare const PROBLEMS: {
15
- readonly refusedToken: "PYXIS_TOKEN was refused: it may be revoked or expired. Create a new access token in the Pyxis dashboard, under Settings, Access tokens.";
16
26
  readonly notFound: "Not found: no project with this id among the projects this token can read. Call list_projects for the ids.";
17
27
  readonly rejected: "The Pyxis API refused the request.";
18
28
  readonly notJson: "The Pyxis API answered with something that is not JSON.";
19
29
  };
20
30
  export declare function requestUrl(apiUrl: string, path: string, query: Query): string;
21
31
  export declare class PyxisApi {
22
- private readonly config;
32
+ private readonly token;
33
+ private readonly apiUrl;
23
34
  private readonly userAgent;
24
35
  private readonly fetch;
25
36
  private readonly timeoutMs;
26
- constructor(config: PyxisConfig, userAgent: string, fetch?: Fetch, timeoutMs?: number);
37
+ private readonly problemTexts;
38
+ constructor(options: PyxisApiOptions);
27
39
  get(path: string, query?: Query): Promise<ApiOutcome>;
28
40
  }
package/dist/pyxis-api.js CHANGED
@@ -1,13 +1,18 @@
1
+ import { userAgent } from './version.js';
1
2
  export const REQUEST_TIMEOUT_MS = 15_000;
2
3
  export const REQUESTS_PER_MINUTE = 60;
3
- const STATUS_BAD_REQUEST = 400;
4
4
  const STATUS_UNAUTHORIZED = 401;
5
5
  const STATUS_NOT_FOUND = 404;
6
6
  const STATUS_TOO_MANY_REQUESTS = 429;
7
+ const FIRST_CLIENT_ERROR = 400;
8
+ const FIRST_SERVER_ERROR = 500;
7
9
  const RETRY_AFTER_HEADER = 'retry-after';
8
10
  const JSON_MEDIA_TYPE = 'application/json';
9
- export const PROBLEMS = {
11
+ export const LOCAL_PROBLEM_TEXTS = {
10
12
  refusedToken: 'PYXIS_TOKEN was refused: it may be revoked or expired. Create a new access token in the Pyxis dashboard, under Settings, Access tokens.',
13
+ unreachableAdvice: 'Check PYXIS_API_URL and the network, then try again.',
14
+ };
15
+ export const PROBLEMS = {
11
16
  notFound: 'Not found: no project with this id among the projects this token can read. Call list_projects for the ids.',
12
17
  rejected: 'The Pyxis API refused the request.',
13
18
  notJson: 'The Pyxis API answered with something that is not JSON.',
@@ -56,37 +61,44 @@ function rateLimitedProblem(response) {
56
61
  : 'in a minute';
57
62
  return `Too many requests: each access token may make ${String(REQUESTS_PER_MINUTE)} requests a minute. Try again ${wait}.`;
58
63
  }
59
- async function problemOf(response) {
64
+ function isRefusal(status) {
65
+ return status >= FIRST_CLIENT_ERROR && status < FIRST_SERVER_ERROR;
66
+ }
67
+ async function problemOf(response, texts) {
60
68
  switch (response.status) {
61
69
  case STATUS_UNAUTHORIZED:
62
- return PROBLEMS.refusedToken;
70
+ return texts.refusedToken;
63
71
  case STATUS_NOT_FOUND:
64
72
  return PROBLEMS.notFound;
65
73
  case STATUS_TOO_MANY_REQUESTS:
66
74
  return rateLimitedProblem(response);
67
- case STATUS_BAD_REQUEST:
68
- return rejectedProblem(response);
69
75
  default:
70
- return `The Pyxis API answered ${String(response.status)}. Try again later.`;
76
+ return isRefusal(response.status)
77
+ ? rejectedProblem(response)
78
+ : `The Pyxis API answered ${String(response.status)}. Try again later.`;
71
79
  }
72
80
  }
73
81
  export class PyxisApi {
74
- config;
82
+ token;
83
+ apiUrl;
75
84
  userAgent;
76
85
  fetch;
77
86
  timeoutMs;
78
- constructor(config, userAgent, fetch = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS) {
79
- this.config = config;
80
- this.userAgent = userAgent;
81
- this.fetch = fetch;
82
- this.timeoutMs = timeoutMs;
87
+ problemTexts;
88
+ constructor(options) {
89
+ this.token = options.token;
90
+ this.apiUrl = options.apiUrl;
91
+ this.userAgent = options.userAgent ?? userAgent();
92
+ this.fetch = options.fetch ?? globalThis.fetch;
93
+ this.timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
94
+ this.problemTexts = options.problemTexts ?? LOCAL_PROBLEM_TEXTS;
83
95
  }
84
96
  async get(path, query = {}) {
85
97
  let response;
86
98
  try {
87
- response = await this.fetch(requestUrl(this.config.apiUrl, path, query), {
99
+ response = await this.fetch(requestUrl(this.apiUrl, path, query), {
88
100
  headers: {
89
- authorization: `Bearer ${this.config.token}`,
101
+ authorization: `Bearer ${this.token}`,
90
102
  accept: JSON_MEDIA_TYPE,
91
103
  'user-agent': this.userAgent,
92
104
  },
@@ -97,11 +109,11 @@ export class PyxisApi {
97
109
  const reason = error instanceof Error ? error.name : 'Error';
98
110
  return {
99
111
  ok: false,
100
- problem: `The Pyxis API at ${this.config.apiUrl} did not answer (${reason}). Check PYXIS_API_URL and the network, then try again.`,
112
+ problem: `The Pyxis API at ${this.apiUrl} did not answer (${reason}). ${this.problemTexts.unreachableAdvice}`,
101
113
  };
102
114
  }
103
115
  if (!response.ok) {
104
- return { ok: false, problem: await problemOf(response) };
116
+ return { ok: false, problem: await problemOf(response, this.problemTexts) };
105
117
  }
106
118
  const body = await readJson(response);
107
119
  return body === undefined ? { ok: false, problem: PROBLEMS.notJson } : { ok: true, body };
@@ -1 +1 @@
1
- {"version":3,"file":"pyxis-api.js","sourceRoot":"","sources":["../src/pyxis-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pyxis-api.js","sourceRoot":"","sources":["../src/pyxis-api.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -3,11 +3,12 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
3
  import type { PyxisApi } from './pyxis-api.js';
4
4
  import { type PyxisTool } from './tools.js';
5
5
  export declare const SERVER_NAME = "pyxis";
6
- export declare const SERVER_INSTRUCTIONS = "Pyxis is privacy-first product analytics: visits, page views, named events and the site's requests to its backend, with no cookies and no personal data. Start with list_projects for the project ids and each project's today, then ask for a range of dates. Every tool only reads.";
6
+ export declare const INSTALL_GUIDE_URL = "https://pyxis-analytics.dev/llms.txt";
7
+ export declare const SERVER_INSTRUCTIONS: string;
7
8
  export declare const UNEXPECTED_ANSWER = "The Pyxis API answered in a shape this server does not recognize. Update pyxis-mcp, or report it at https://github.com/samuelcsantana/pyxis-mcp/issues.";
8
9
  export interface ServerOptions {
9
- readonly version: string;
10
+ readonly version?: string;
10
11
  readonly now?: () => Date;
11
12
  }
12
13
  export declare function runTool(tool: PyxisTool, api: PyxisApi, args: unknown, now: Date): Promise<CallToolResult>;
13
- export declare function createServer(api: PyxisApi, options: ServerOptions): McpServer;
14
+ export declare function createServer(api: PyxisApi, options?: ServerOptions): McpServer;
package/dist/server.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { PYXIS_TOOLS, ToolInputError, VISITOR_TEXT_NOTE, } from './tools.js';
3
+ import { PACKAGE_VERSION } from './version.js';
3
4
  export const SERVER_NAME = 'pyxis';
4
- export const SERVER_INSTRUCTIONS = "Pyxis is privacy-first product analytics: visits, page views, named events and the site's requests to its backend, with no cookies and no personal data. Start with list_projects for the project ids and each project's today, then ask for a range of dates. Every tool only reads.";
5
+ export const INSTALL_GUIDE_URL = 'https://pyxis-analytics.dev/llms.txt';
6
+ export const SERVER_INSTRUCTIONS = "Pyxis is privacy-first product analytics: visits, page views, named events and the site's requests to its backend, with no cookies and no personal data. Start with list_projects for the project ids and each project's today, then ask for a range of dates. Every tool only reads. " +
7
+ 'An empty list means no project was created yet, or none was shared with this person: projects are created in the Pyxis dashboard. ' +
8
+ `A project whose first_event_at is null has received nothing, so its site does not run the tracker yet. No tool here can install it; ${INSTALL_GUIDE_URL} explains how (the npm package pyxis-analytics, started with one of the public keys project_settings lists) and the rules an integration must follow.`;
5
9
  export const UNEXPECTED_ANSWER = 'The Pyxis API answered in a shape this server does not recognize. Update pyxis-mcp, or report it at https://github.com/samuelcsantana/pyxis-mcp/issues.';
6
10
  const READ_ONLY = {
7
11
  readOnlyHint: true,
@@ -45,9 +49,9 @@ export async function runTool(tool, api, args, now) {
45
49
  const report = tool.report(outcome.body, now);
46
50
  return report === undefined ? failure(UNEXPECTED_ANSWER) : success(report);
47
51
  }
48
- export function createServer(api, options) {
52
+ export function createServer(api, options = {}) {
49
53
  const now = options.now ?? (() => new Date());
50
- const server = new McpServer({ name: SERVER_NAME, version: options.version }, { instructions: SERVER_INSTRUCTIONS });
54
+ const server = new McpServer({ name: SERVER_NAME, version: options.version ?? PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
51
55
  for (const tool of PYXIS_TOOLS) {
52
56
  server.registerTool(tool.name, {
53
57
  title: tool.title,
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pyxis-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for Pyxis, privacy-first product analytics: lets an AI agent read your dashboard's numbers with a read-only token.",
3
+ "version": "0.3.0",
4
+ "description": "MCP server for Pyxis, privacy-first product analytics: lets an AI agent read your dashboard's numbers with a read-only token, locally over stdio or hosted over HTTP.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "model-context-protocol",
@@ -21,6 +21,15 @@
21
21
  "license": "MIT",
22
22
  "author": "Samuel Santana",
23
23
  "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
24
33
  "engines": {
25
34
  "node": ">=22"
26
35
  },