@pixlcore/xycli 1.1.0 → 1.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.
Files changed (45) hide show
  1. package/.dockerignore +13 -0
  2. package/.github/workflows/docker.yml +64 -0
  3. package/.github/workflows/offline-tests.yml +1 -1
  4. package/Dockerfile +45 -0
  5. package/README.md +21 -0
  6. package/docs/help.md +23 -0
  7. package/docs/mcp.md +224 -0
  8. package/docs/two-way-delete-tutorial.md +5 -2
  9. package/index.js +11 -1
  10. package/lib/config.js +7 -2
  11. package/lib/mcp/context.js +52 -0
  12. package/lib/mcp/registry.js +27 -0
  13. package/lib/mcp/results.js +14 -0
  14. package/lib/mcp/schemas.js +67 -0
  15. package/lib/mcp/server.js +28 -0
  16. package/lib/mcp/tools/comment-ticket.js +11 -0
  17. package/lib/mcp/tools/create-event.js +17 -0
  18. package/lib/mcp/tools/create-ticket.js +11 -0
  19. package/lib/mcp/tools/delete-event.js +8 -0
  20. package/lib/mcp/tools/delete-ticket.js +8 -0
  21. package/lib/mcp/tools/get-event-options.js +19 -0
  22. package/lib/mcp/tools/get-event.js +8 -0
  23. package/lib/mcp/tools/get-job.js +8 -0
  24. package/lib/mcp/tools/list-events.js +17 -0
  25. package/lib/mcp/tools/list-servers.js +17 -0
  26. package/lib/mcp/tools/run-event.js +10 -0
  27. package/lib/mcp/tools/search-alerts.js +12 -0
  28. package/lib/mcp/tools/search-jobs.js +12 -0
  29. package/lib/mcp/tools/search-snapshots.js +12 -0
  30. package/lib/mcp/tools/take-snapshot.js +15 -0
  31. package/lib/mcp/tools/update-event.js +12 -0
  32. package/lib/mcp/tools/update-ticket.js +11 -0
  33. package/lib/mcp/transports/http.js +76 -0
  34. package/lib/mcp/transports/stdio.js +7 -0
  35. package/lib/mcp.js +117 -0
  36. package/package.json +9 -4
  37. package/test/README.md +2 -0
  38. package/test/config.test.js +22 -0
  39. package/test/fixtures/mcp/localhost.crt +19 -0
  40. package/test/fixtures/mcp/localhost.key +28 -0
  41. package/test/helpers/mcp.js +78 -0
  42. package/test/mcp-http-unit.test.js +188 -0
  43. package/test/mcp-stdio-unit.test.js +194 -0
  44. package/test/mcp-tools-unit.test.js +212 -0
  45. package/test/mcp.test.js +212 -0
package/.dockerignore ADDED
@@ -0,0 +1,13 @@
1
+ # Include only the files needed to run xyCLI and display its documentation.
2
+ # In particular, never copy host node_modules, credentials, or test fixtures.
3
+ *
4
+ !package.json
5
+ !package-lock.json
6
+ !index.js
7
+ !lib/
8
+ !lib/**
9
+ !docs/
10
+ !docs/**
11
+ !README.md
12
+ !CHANGELOG.md
13
+ !logo.webp
@@ -0,0 +1,64 @@
1
+ name: Publish Docker Image
2
+
3
+ on:
4
+ push:
5
+ tags: [ '*' ]
6
+
7
+ permissions:
8
+ contents: read
9
+ packages: write
10
+ # Required for GitHub's keyless Sigstore artifact attestation.
11
+ id-token: write
12
+ attestations: write
13
+ # Required to register the attested image in GitHub's linked artifact inventory.
14
+ artifact-metadata: write
15
+
16
+ jobs:
17
+ build-and-push:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+
22
+ - name: Set up QEMU
23
+ uses: docker/setup-qemu-action@v4
24
+
25
+ - name: Set up Buildx
26
+ uses: docker/setup-buildx-action@v4
27
+
28
+ - name: Log in to GHCR
29
+ uses: docker/login-action@v4
30
+ with:
31
+ registry: ghcr.io
32
+ username: ${{ github.actor }}
33
+ password: ${{ secrets.GITHUB_TOKEN }}
34
+
35
+ - name: Extract image metadata
36
+ id: meta
37
+ uses: docker/metadata-action@v6
38
+ with:
39
+ images: ghcr.io/${{ github.repository }}
40
+ tags: |
41
+ type=raw,value=latest,enable={{is_default_branch}}
42
+ type=ref,event=tag
43
+ type=sha
44
+
45
+ - name: Build and push (multi-arch)
46
+ id: push
47
+ uses: docker/build-push-action@v7
48
+ with:
49
+ context: .
50
+ file: ./Dockerfile
51
+ platforms: linux/amd64,linux/arm64
52
+ push: true
53
+ tags: ${{ steps.meta.outputs.tags }}
54
+ labels: ${{ steps.meta.outputs.labels }}
55
+ cache-from: type=gha
56
+ cache-to: type=gha,mode=max
57
+
58
+ # Sign the exact multi-platform image digest that Buildx pushed to GHCR.
59
+ - name: Generate build provenance attestation
60
+ uses: actions/attest@v4
61
+ with:
62
+ subject-name: ghcr.io/${{ github.repository }}
63
+ subject-digest: ${{ steps.push.outputs.digest }}
64
+ push-to-registry: true
@@ -19,7 +19,7 @@ jobs:
19
19
  - ubuntu-latest
20
20
  - windows-latest
21
21
  node:
22
- - 20.19.0
22
+ - 22
23
23
  - 24
24
24
 
25
25
  steps:
package/Dockerfile ADDED
@@ -0,0 +1,45 @@
1
+ FROM node:24-bookworm-slim
2
+
3
+ LABEL org.opencontainers.image.source="https://github.com/pixlcore/xycli"
4
+ LABEL org.opencontainers.image.description="The xyOps command-line interface and MCP server."
5
+ LABEL org.opencontainers.image.licenses="MIT"
6
+
7
+ ENV DEBIAN_FRONTEND=noninteractive
8
+
9
+ # Keep the runtime small: no worker tools, compilers, or Docker CLI are needed.
10
+ # Remove apt caches in the same layer that installs the certificate bundle.
11
+ RUN set -eu; \
12
+ apt-get update; \
13
+ apt-get install -y --no-install-recommends ca-certificates; \
14
+ apt-get clean; \
15
+ rm -rf /var/lib/apt/lists/*
16
+
17
+ WORKDIR /opt/xycli
18
+
19
+ ENV NODE_ENV=production
20
+
21
+ # Install the tagged checkout's locked dependencies, rather than fetching an
22
+ # unrelated CLI release from npm. Source edits can reuse the dependency layer.
23
+ COPY package.json package-lock.json ./
24
+ RUN npm ci --omit=dev \
25
+ && npm cache clean --force
26
+
27
+ # .dockerignore includes only runtime source and documentation, so local
28
+ # node_modules links, test fixtures, and private configuration stay outside.
29
+ COPY . .
30
+
31
+ # Keep the application read-only for the runtime user, with a writable working
32
+ # directory for commands that export or sync files.
33
+ RUN ln -s /opt/xycli/index.js /usr/local/bin/xy \
34
+ && mkdir -p /workspace \
35
+ && chown node:node /workspace
36
+
37
+ USER node
38
+ WORKDIR /workspace
39
+
40
+ EXPOSE 8080/tcp
41
+
42
+ # Arguments after the image name replace CMD, allowing any xyCLI command.
43
+ # HTTP needs an explicit allowed_hosts list supplied in XYOPS_MCP or cli.json.
44
+ ENTRYPOINT ["xy"]
45
+ CMD ["mcp", "serve", "--host", "0.0.0.0", "--port", "8080"]
package/README.md CHANGED
@@ -23,6 +23,7 @@ Browse the [screenshot gallery](docs/screenshots.md) for more views, or jump to
23
23
  ## Contents
24
24
 
25
25
  - [Getting started](#getting-started)
26
+ - [Docker](#docker)
26
27
  - [Configuration](#configuration)
27
28
  - [Global CLI switches](#global-cli-switches)
28
29
  - [Tutorial: your first Event, job, and Web Hook](docs/tutorial.md)
@@ -33,6 +34,7 @@ Browse the [screenshot gallery](docs/screenshots.md) for more views, or jump to
33
34
  - [Sync your automation with local files](#sync-your-automation-with-local-files)
34
35
  - [System operations and logs](#system-operations-and-logs)
35
36
  - [Documentation and API access](#documentation-and-api-access)
37
+ - [MCP server for AI applications](docs/mcp.md)
36
38
 
37
39
  ## Getting started
38
40
 
@@ -67,6 +69,25 @@ xy
67
69
 
68
70
  You should see the dashboard. If you need to set up xyOps itself, start with its [Getting Started guide](https://docs.xyops.io/start). See [API Keys](https://docs.xyops.io/api#api-keys) for authentication details and [Privileges](https://docs.xyops.io/privileges) for permissions.
69
71
 
72
+ ## Docker
73
+
74
+ The `ghcr.io/pixlcore/xycli` image includes Node.js and xyCLI, so you only need Docker installed on your machine. It supports Linux AMD64 and ARM64. Images are built from the source checkout on Git tag pushes and published with the Git tag, `latest`, and a `sha-` tag.
75
+
76
+ The image's entrypoint is `xy`. Pass a CLI command after the image name, for example:
77
+
78
+ ```sh
79
+ docker run --rm -it \
80
+ -e XYOPS_BASE_URL="https://xyops.example.com" \
81
+ -e XYOPS_API_KEY="YOUR_API_KEY" \
82
+ ghcr.io/pixlcore/xycli:latest events
83
+ ```
84
+
85
+ Without a command override, the container starts the MCP HTTP server. See [Running MCP with Docker](docs/mcp.md#running-with-docker) for a complete startup command and STDIO usage.
86
+
87
+ The container runs as the `node` user (UID 1000), with `/workspace` as its working directory. For file exports, imports, or sync, mount your working files there with `-v "$PWD:/workspace"` and ensure that user can access them. You can also mount an existing CLI configuration file read-only at `/home/node/.config/xyops/cli.json`. Custom sync completion commands need any external programs they invoke; Git and worker tools are not bundled in this image.
88
+
89
+ To upgrade, pull the new image and recreate the container. Use a specific Git tag instead of `latest` when you want to pin a release. For a local build from this repository, run `docker build -t xycli:local .` and substitute `xycli:local` in the examples.
90
+
70
91
  ## Configuration
71
92
 
72
93
  xyCLI loads optional JSON configuration files on every invocation, then applies environment variables. Settings are merged in this order:
package/docs/help.md CHANGED
@@ -25,6 +25,8 @@ xy tickets
25
25
  xy hooks
26
26
  xy marketplace
27
27
  xy upgrade
28
+ xy mcp stdio
29
+ xy mcp serve --port 8080
28
30
  xy event EVENT_ID --export event.json
29
31
  xy import event.json
30
32
  xy sync ./ --up events,plugins --dry
@@ -394,6 +396,8 @@ Updates load the user file separately and apply only the settings specified on t
394
396
 
395
397
  Configuration loads the machine file first, then the user file, then `XYOPS_`-prefixed environment variables. The machine and user files are `/etc/xyops/cli.json` and `~/.config/xyops/cli.json` on Unix and macOS, or `%PROGRAMDATA%\xyops\cli.json` and `%USERPROFILE%\.config\xyops\cli.json` on Windows. Later sources override matching top-level settings. Environment overrides still take precedence over saved user settings. Common keys include `api_key`, `base_url`, `temp_dir`, `color`, `suggest`, `items_per_page`, `cache_ttl`, `raw`, and `invisible`.
396
398
 
399
+ Environment values that look like JSON objects or arrays are parsed automatically for any setting. For example, `XYOPS_SYNC='{"up":["events"],"down":false}'` supplies a structured `sync` setting. Boolean and numeric values keep their usual conversions. Malformed JSON-looking values report the environment variable name. Objects and arrays are serialized as JSON when the CLI exports its effective configuration back into the environment.
400
+
397
401
  ## doc
398
402
 
399
403
  Fetch and display xyOps documentation in the terminal. Run the command without a document name to browse the documentation index. Specify a document to read it in full, or add a chapter slug to focus on one section and all of its sub-sections.
@@ -2635,3 +2639,22 @@ Default output shows complete bracket-delimited log lines using the server's con
2635
2639
  The search selects the last N matches before applying the display order, so `--sort date_asc` does not select the earliest N matches. There is no numbered pagination; use `--rows` rather than `--limit`, `--offset`, or `--page`. The summary's total row count includes non-matching lines.
2636
2640
 
2637
2641
  Missing logs or archives return an empty result. `--dry` previews the search. To view a particular Job's output instead, use `xy job log JOB_ID`.
2642
+
2643
+ # mcp
2644
+
2645
+ Connect AI applications to xyOps using xyOps API keys. Requires Node.js 20 or newer.
2646
+
2647
+ ```sh
2648
+ xy mcp stdio
2649
+ xy mcp serve --port 8080
2650
+ xy mcp stdio --read-only
2651
+ xy mcp serve --port 8443 --tls-cert /path/to/server.crt --tls-key /path/to/server.key
2652
+ ```
2653
+
2654
+ STDIO is for applications that launch a local process. HTTP exposes `/mcp` on `127.0.0.1` by default. The `mcp` prefix is required; pass the port with `--port`. Use `--host` to choose another bind address. Every HTTP request requires the client's xyOps API key as a bearer token, including loopback connections. Non-loopback binds additionally require explicit `mcp.allowed_hosts`; there is no separate MCP authentication token.
2655
+
2656
+ Both modes expose event discovery, creation, updates, deletion, execution and job inspection/search. `--read-only` omits and blocks mutation/execution tools. Every API call still uses your key's existing privileges and resource restrictions.
2657
+
2658
+ STDIO uses your configured `XYOPS_API_KEY`. HTTP clients send their own xyOps API key as `Authorization: Bearer YOUR_XYOPS_API_KEY`; each request forwards that key to xyOps, with no fallback to the server's configured key. MCP owns stdin/stdout and does not support interactive CLI options such as `--dry` or `--format`. Diagnostics go to stderr.
2659
+
2660
+ See [MCP Server](mcp.md) for configuration, tools, deployment and tool-authoring details.
package/docs/mcp.md ADDED
@@ -0,0 +1,224 @@
1
+ # MCP Server
2
+
3
+ xyCLI connects AI applications to xyOps through the Model Context Protocol (MCP). Both transports use your normal CLI connection settings. STDIO uses your configured API key; HTTP uses the key supplied by each client. xyOps enforces that key's privileges and resource restrictions on every API operation.
4
+
5
+ ## Quick start
6
+
7
+ First, if you haven't already done so, follow [Getting started](../README.md#getting-started) to install Node.js and xyCLI, then configure your xyOps connection and API key. Alternatively, use the [Docker image](#running-with-docker), which includes Node.js and xyCLI.
8
+
9
+ For an application that launches a local MCP process, configure its executable as `xy` and its arguments as `mcp`, `stdio`:
10
+
11
+ ```sh
12
+ xy mcp stdio
13
+ ```
14
+
15
+ The STDIO server uses your existing xyCLI authentication: the `XYOPS_API_KEY` environment variable or the `api_key` setting in your `cli.json` configuration file. The environment variable takes precedence.
16
+
17
+ The application launches this command and communicates through its standard streams. stdout contains only MCP messages; diagnostics go to stderr. If the application cannot find `xy`, use the absolute path reported by `which xy` on macOS/Linux or `where xy` on Windows.
18
+
19
+ For a client that connects over HTTP, start a foreground server like so:
20
+
21
+ ```sh
22
+ xy mcp serve --port 8080
23
+ ```
24
+
25
+ Connect the client to `http://127.0.0.1:8080/mcp` and configure its `Authorization` header as `Bearer YOUR_XYOPS_API_KEY`. The `mcp` prefix is required, and the port is a named option. Stop the server with Ctrl-C.
26
+
27
+ MCP requires Node.js 20 or newer. This server supports the [2026-07-28 protocol](https://blog.modelcontextprotocol.io/posts/2026-07-28/) and compatible 2025-era clients over STDIO and stateless Streamable HTTP. Legacy HTTP+SSE endpoints and persistent HTTP sessions are not provided.
28
+
29
+ ## Running with Docker
30
+
31
+ The general-purpose `ghcr.io/pixlcore/xycli` image starts the MCP HTTP server by default. With Docker installed, this single command supplies all required server settings:
32
+
33
+ ```sh
34
+ docker run --rm --init --name xycli-mcp \
35
+ -p 127.0.0.1:8080:8080 \
36
+ -e XYOPS_BASE_URL="https://xyops.example.com" \
37
+ -e XYOPS_MCP='{"allowed_hosts":["localhost","127.0.0.1","[::1]"]}' \
38
+ ghcr.io/pixlcore/xycli:latest
39
+ ```
40
+
41
+ Replace `XYOPS_BASE_URL` with your xyOps URL. Connect your MCP client to `http://127.0.0.1:8080/mcp` and configure its `Authorization` header as `Bearer YOUR_XYOPS_API_KEY`. HTTP mode does not need `XYOPS_API_KEY` in the container; each client supplies its own key.
42
+
43
+ The server listens on `0.0.0.0:8080` inside the container, while the example publishes it only on your host's loopback interface. For network access, configure `allowed_hosts` for the hostname your clients use, publish the appropriate interface, and provide HTTPS through a reverse proxy or the server's TLS options. `allowed_hosts` contains hostnames without port numbers, not the container's bind address.
44
+
45
+ If xyOps runs on your Docker host, use `http://host.docker.internal:5522` as the base URL. On Linux, also add `--add-host=host.docker.internal:host-gateway`. `localhost` inside the container refers to the container itself.
46
+
47
+ For **STDIO**, configure your MCP client to launch this command instead:
48
+
49
+ ```sh
50
+ docker run --rm --init -i \
51
+ -e XYOPS_BASE_URL="https://xyops.example.com" \
52
+ -e XYOPS_API_KEY="YOUR_XYOPS_API_KEY" \
53
+ ghcr.io/pixlcore/xycli:latest mcp stdio
54
+ ```
55
+
56
+ Keep `-i` so the client can write protocol messages to stdin, and omit `-t` so no terminal is allocated. STDIO uses the supplied API key and needs no published port or HTTP Host allowlist.
57
+
58
+ Arguments after the image name replace the default `mcp serve --host 0.0.0.0 --port 8080` command. For example, append `mcp serve --host 0.0.0.0 --port 8080 --read-only` to the HTTP example for read-only access. The same image runs other CLI commands; see [Docker](../README.md#docker) for file mounts, configuration, and upgrades.
59
+
60
+ ## Testing with MCP Inspector
61
+
62
+ [MCP Inspector](https://github.com/modelcontextprotocol/inspector) provides a browser interface for browsing tools, inspecting their inputs, and making tool calls.
63
+
64
+ For **STDIO**, launch Inspector with xyCLI as its server command:
65
+
66
+ ```sh
67
+ npx @modelcontextprotocol/inspector xy mcp stdio
68
+ ```
69
+
70
+ Inspector launches xyCLI using your existing connection settings and API key. Open the browser URL printed in the terminal (if it doesn't automatically open it for you) and connect to the server.
71
+
72
+ For **HTTP**, start xyCLI in one terminal:
73
+
74
+ ```sh
75
+ xy mcp serve --port 8080
76
+ ```
77
+
78
+ Then launch Inspector in another terminal:
79
+
80
+ ```sh
81
+ npx @modelcontextprotocol/inspector
82
+ ```
83
+
84
+ Open its browser interface and configure the connection:
85
+
86
+ | Setting | Value |
87
+ | --- | --- |
88
+ | Transport | Streamable HTTP |
89
+ | URL | `http://127.0.0.1:8080/mcp` |
90
+ | Custom header name | `Authorization` |
91
+ | Custom header value | `Bearer YOUR_XYOPS_API_KEY` |
92
+
93
+ After connecting with either transport, open the Tools view and try `list_events` or `get_event_options`. Select a tool to inspect its inputs, submit a call, and view the result.
94
+
95
+ ## Authentication and permissions
96
+
97
+ Configure the upstream URL through your normal CLI JSON file or `XYOPS_BASE_URL`. For STDIO, supply the API key through `api_key` in that file or `XYOPS_API_KEY`, just like the normal CLI. Keep keys in private configuration or environment variables, rather than prompts or command-line arguments.
98
+
99
+ Each process connects to one configured xyOps installation. HTTP clients supply their own xyOps API keys using `Authorization: Bearer YOUR_XYOPS_API_KEY` on every request. Each tool forwards that client's key to xyOps as `X-API-Key`, including when a tool makes multiple API calls. Concurrent clients keep their credentials and permissions separate.
100
+
101
+ HTTP mode does not require a server-side API key and never falls back to one. Missing or malformed bearer headers receive HTTP 401. xyOps rejects invalid keys and unauthorized API operations; these failures are returned as MCP tool errors. Tool discovery is local and does not validate the key upstream or filter tools by its privileges.
102
+
103
+ A key can permit reading and running existing events without granting `create_events`, `edit_events`, or `delete_events`. This is read-and-execute access. For strict read-only access, also omit `run_jobs` and other write privileges. You can additionally restrict MCP itself:
104
+
105
+ ```sh
106
+ xy mcp stdio --read-only
107
+ xy mcp serve --port 8080 --read-only
108
+ ```
109
+
110
+ Read-only mode omits all mutation/execution tools and rejects direct attempts to call them. Tool annotations describe behavior; actual xyOps privileges remain authoritative.
111
+
112
+ There is no separate MCP authentication token. HTTP clients must support configured authorization headers. OAuth discovery and interactive login are not provided in this version.
113
+
114
+ ## HTTP configuration
115
+
116
+ Add an `mcp` object to your existing CLI configuration:
117
+
118
+ ```json
119
+ {
120
+ "mcp": {
121
+ "host": "127.0.0.1",
122
+ "port": 8080,
123
+ "read_only": false,
124
+ "max_page_size": 100,
125
+ "max_request_bytes": 1048576,
126
+ "max_result_bytes": 1048576
127
+ }
128
+ }
129
+ ```
130
+
131
+ | Setting | Default | Purpose |
132
+ | --- | --- | --- |
133
+ | `host` | `127.0.0.1` | Listener address. Override with `--host`. |
134
+ | `port` | `8080` | Listener port. Override with `--port`. |
135
+ | `allowed_hosts` | `localhost`, `127.0.0.1`, `[::1]` | Accepted Host header hostnames, without port numbers. Enclose IPv6 addresses in brackets. |
136
+ | `allowed_origins` | Empty array | Exact allowed browser origins, including scheme and port. Requests without an Origin header are accepted. |
137
+ | `read_only` | `false` | Advertise and accept only read tools. |
138
+ | `max_page_size` | `100` | Maximum rows per page, from 1 to 100. |
139
+ | `max_request_bytes` | `1048576` | Maximum protocol input size. |
140
+ | `max_result_bytes` | `1048576` | Maximum serialized tool data size. Oversized results return an explicit error. |
141
+ | `tls_cert`, `tls_key` | Unset | PEM certificate and key files for HTTPS. Supply both together. |
142
+
143
+ Configuration environment values that look like JSON objects or arrays are parsed as JSON, including `XYOPS_MCP`. Scalar settings accept flat environment overrides such as `XYOPS_MCP_PORT`, `XYOPS_MCP_READ_ONLY`, and `XYOPS_MCP_MAX_RESULT_BYTES`. Configure host/origin arrays in the JSON file. Command-line options override environment variables, which override the `mcp` object.
144
+
145
+ A non-loopback bind requires an explicit `mcp.allowed_hosts` list. Bearer API keys are required on every HTTP request, including loopback connections. Use HTTPS or a trusted reverse proxy providing TLS for network access. A non-loopback HTTP bind does not automatically encrypt traffic. No permissive browser CORS policy is enabled.
146
+
147
+ For IPv6 loopback, use `xy mcp serve --host ::1 --port 8080` and connect to `http://[::1]:8080/mcp`. The bind address uses no brackets; URLs and `allowed_hosts` entries use brackets.
148
+
149
+ For direct HTTPS:
150
+
151
+ ```sh
152
+ xy mcp serve --port 8443 --tls-cert /path/to/server.crt --tls-key /path/to/server.key
153
+ ```
154
+
155
+ This opens only the HTTPS listener. Restart after replacing certificate files. Unknown routes return 404; local files are never served.
156
+
157
+ ## Tools
158
+
159
+ | Tool | Purpose |
160
+ | --- | --- |
161
+ | `list_events` | Find events/workflows by title, category, type or enabled state. Returns summaries and revisions. |
162
+ | `get_event` | Fetch a definition and revision. Optional `fields` limits returned properties. |
163
+ | `get_event_options` | Discover categories, target groups and event plugins. Optional `plugin_id` adds parameter definitions. |
164
+ | `create_event` | Create an ordinary event or workflow from an explicit definition. |
165
+ | `update_event` | Apply changes using an exact ID and the revision returned by `get_event`. |
166
+ | `delete_event` | Delete an event while retaining historical jobs. Active jobs prevent deletion. |
167
+ | `run_event` | Launch an enabled event with a manual trigger, optionally supplying parameters and input data. |
168
+ | `get_job` | Inspect job state, completion and output. Optional `fields` selects other job properties. |
169
+ | `search_jobs` | Search completed jobs with query, pagination and sorting. |
170
+ | `create_ticket` | Create a ticket with a subject, Markdown body and optional classification/assignment. |
171
+ | `update_ticket` | Apply only supplied fields to a ticket by exact ID. |
172
+ | `delete_ticket` | Permanently delete a ticket by exact ID. |
173
+ | `comment_ticket` | Append a Markdown comment with the authenticated key as its author. |
174
+ | `search_alerts` | Search active and historical alert invocations, rather than alert definitions. |
175
+ | `search_snapshots` | Search server/group snapshots, optionally including verbose monitoring details. |
176
+ | `take_snapshot` | Capture the latest monitoring data for exactly one server or group. |
177
+ | `list_servers` | List connected workers, optionally filtering by hostname, group or enabled state. |
178
+
179
+ Pages default to 25 rows, capped by `max_page_size`. Events, servers and lookup collections are paginated locally after fetching the authorized collection; job, alert and snapshot searches are paginated on the server. Each lookup collection has its own total and uses the requested offset/limit.
180
+
181
+ Updates, deletions and executions require exact IDs. Event creation requires an explicit `enabled` value. Ordinary events require category, plugin, targets and a server-selection algorithm. Workflows require a workflow object with nodes and connections. xyOps validates the full definition and permissions.
182
+
183
+ Event updates preserve omitted top-level fields. Supplied objects and arrays replace their saved values, including `params`, `triggers` and `workflow`. Read the definition first and include the values you want to retain. Stale revisions are rejected instead of silently overwriting newer changes.
184
+
185
+ `run_event` returns immediately with a job ID. Use `get_job` until `final` is true, or search with `event:EVENT_ID` or `tags:_error`. Launches accept parameter overrides and `input.data`, but not arbitrary event overrides, test-mode bypasses or local-file uploads.
186
+
187
+ ### Tickets
188
+
189
+ Create a ticket with `{"ticket":{"subject":"Backup failed","body":"Investigating the logs.","type":"issue"}}`. xyOps generates its ID, number and initial history. Ticket types and statuses follow your xyOps configuration, including custom values. This initial tool set supports text and metadata; file attachments, automatic job/alert templates and attached event definitions are not exposed.
190
+
191
+ Use `update_ticket` with `{"id":"TICKET_ID","changes":{"status":"closed"}}`. Omitted fields remain unchanged, while arrays such as `assignees`, `cc`, `notify` and `tags` replace their entire lists. Empty arrays clear those lists; an empty `category` or `server` clears that link. `due` is an absolute Unix timestamp in seconds (1980 or later), or `0` to clear it. Unlike events, tickets have no revision precondition, so concurrent edits to the same field can overwrite each other.
192
+
193
+ Use `comment_ticket` with `{"id":"TICKET_ID","body":"The backup is working again."}`. xyOps assigns the comment author and timestamp. Create/update return ticket summaries; comment also returns the new comment. Creating, updating and commenting may send notifications according to xyOps settings. These operations require `create_tickets`, `edit_tickets` and `delete_tickets` as appropriate; comments require `edit_tickets`.
194
+
195
+ ### Monitoring
196
+
197
+ `search_alerts` accepts queries such as `active:true`, `server:SERVER_ID` or `start:>=today`. `search_snapshots` accepts queries such as `type:group`, `groups:GROUP_ID` or `source:alert`. Both support `query`, `offset`, `limit`, `sort_by` and `sort_dir` (`1` ascending, `-1` descending), defaulting to newest IDs first. xyOps applies the API key's resource restrictions. Snapshot search omits heavy nested details unless `verbose` is `true`; use small pages with verbose results to stay within the response size limit.
198
+
199
+ `list_servers` returns connected worker summaries and IDs, excluding offline servers and conductor peers. `hostname` matches a case-insensitive substring, `group` matches an exact group ID, and `enabled` filters the enabled flag.
200
+
201
+ Use `take_snapshot` with either `{"server":"SERVER_ID"}` or `{"group":"GROUP_ID"}`, never both. This requires `create_snapshots` and stores the most recent monitoring sample; it does not start a watch or force a new sample. The result includes the snapshot ID and target. Group IDs can be found in server summaries or `get_event_options`.
202
+
203
+ API requests are not automatically retried. A lost connection after a mutation may leave its outcome uncertain; inspect xyOps before repeating it. Cancellation or shutdown does not undo accepted mutations or abort jobs already launched.
204
+
205
+ ## Adding a tool
206
+
207
+ Every bundled `.js` file in `lib/mcp/tools/` exports:
208
+
209
+ | Function | Contract |
210
+ | --- | --- |
211
+ | `describe()` | Synchronously returns `name`, `description`, a plain JSON `inputSchema`, and behavioral `annotations`. No API calls or other side effects. |
212
+ | `async run(args, context)` | Returns a JSON-serializable object or throws. Never writes stdout, reads stdin, or exits the process. |
213
+
214
+ The registry sorts and loads files once, checks exports/names, and compiles schemas. Helpers belong outside the tools directory. Duplicate names or malformed definitions stop startup.
215
+
216
+ The context supplies `callAPI(name, params)`, `signal`, `log`, and `limits`. HTTP scopes this context to the incoming key. Tool modules must use `context.callAPI()` so every upstream call uses the correct credentials. Add newly required methods to the shared boundary's read/write API allowlists. Calls use the existing SDK. Cancellation prevents subsequent API calls but cannot undo requests already sent.
217
+
218
+ Both transports invoke the same modules. A tool can combine API calls, simplify inputs and select useful outputs. The runtime validates arguments and returns structured data plus JSON text. Provide pagination or field selection for large results. Keep credentials and raw transport envelopes out of outputs and diagnostics.
219
+
220
+ ## Tests
221
+
222
+ `npm run test:offline` includes schema, permission-boundary and wire-level STDIO/HTTP/HTTPS checks using fixtures. The TLS key in `test/fixtures/mcp/` is a public test fixture, never a deployment credential.
223
+
224
+ `node test.js mcp` runs the live suite against `http://localhost:5522`. It requires administrator test credentials, an enabled category, the stock Shell Plugin and a connected worker. It creates disposable events/API keys and a draft ticket, runs a harmless echo job, takes server/group snapshots, verifies restricted-key behavior over STDIO and concurrent HTTP requests, and cleans up its records. Monitoring tests require a worker in a group with a saved monitoring sample. The ticket remains a draft to suppress notifications; the suite deletes only snapshots it created and verifies their removal.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This tutorial keeps Events, workflows, and Plugins in both xyOps and Git. Existing definitions can be edited on either side, and deletions can start on either side. New definitions are created in the xyOps UI and exported to Git. Every sync runs because Git accepted a push or xyOps recorded an activity. There are no cron jobs or recurring sync schedules.
4
4
 
5
- Use xyCLI version **v1.1.0+** (with down-sync delete support) and xyOps version **v1.1.3+** (with System Activity triggers). Both features must be available before you enable this workflow.
5
+ Use xyCLI version **v1.1.1+** (with down-sync delete support) and xyOps version **v1.1.4+** (with System Activity triggers and exclusions). Both features must be available before you enable this workflow.
6
6
 
7
7
  > [!IMPORTANT]
8
8
  > **Two-way deletion requires authoritative triggers in both directions.** Run `--up --delete` only in response to an accepted Git change, and run `--down --delete` only in response to xyOps activity. Each invocation has exactly one authoritative side. A single command with both `--up` and `--down` cannot use `--delete`.
@@ -24,7 +24,7 @@ Use one persistent Git checkout on a machine running xySat. Target all three xyO
24
24
 
25
25
  You will need:
26
26
 
27
- - A xyOps API Key that can see the **complete** Event and Plugin inventories, update and delete those types, and update global sync state. A restricted or partial inventory is unsafe for up-only deletion.
27
+ - A dedicated xyOps API Key for xyCLI that can see the **complete** Event and Plugin inventories, update and delete those types, and update global sync state. A restricted or partial inventory is unsafe for up-only deletion. You will select this same key in the System Activity trigger exclusions below.
28
28
  - A dedicated repository and a persistent checkout, such as `/srv/xyops-automation`, with noninteractive Git pull and push access.
29
29
  - A Git author identity for commits generated by xyOps.
30
30
  - Shared xyOps job capacity and queue limits on all three Events.
@@ -171,6 +171,8 @@ Create three Shell Plugin Events in xyOps, initially disabled. Target only the s
171
171
  | **Export creations from xyOps** | System Activity | `event_create`, `plugin_create` | `/usr/local/sbin/xyops-git-sync create` |
172
172
  | **Apply accepted Git pushes** | Manual | No activity selection | `/usr/local/sbin/xyops-git-sync up` |
173
173
 
174
+ On **each of the two System Activity triggers**, open **Exclude Users / Keys** and select the dedicated xyCLI sync API Key configured on the worker in step 1. xyOps records the key's ID as the activity initiator and skips that trigger when the ID is excluded. This prevents an upsync API update or deletion from launching a redundant downsync job. Leave ordinary users out of the exclusion list so their UI changes still launch the appropriate job. If more than one API Key can make sync writes, exclude each of those keys on both triggers.
175
+
174
176
  The update activities make xyOps UI edits flow back to Git too. The separate creation Event runs `sync setup --new`, because ordinary downsync updates existing local sources but does not create files for new xyOps definitions. The setup command's `--down_cmd` runs only when it writes files; the down-delete command's `--down_cmd` runs only after local changes. Neither creates an empty Git commit on a no-op run.
175
177
 
176
178
  The Git Event has an enabled **Manual** trigger so a Git hook or verified webhook receiver can launch it with `xy run` or the [`run_event` API](https://docs.xyops.io/api#run_event). It has no schedule. The Git push is still the authoritative reason for that run.
@@ -237,6 +239,7 @@ Run these tests with disposable Events and Plugins, not production definitions:
237
239
  3. Delete it in xyOps. Confirm that the **down** Event removes the XYPDF and owned neighbor files and pushes the deletion.
238
240
  4. Edit an existing definition in Git and push. Confirm that the accepted-push trigger runs **up** and updates xyOps.
239
241
  5. Delete a different existing definition in Git and push. Confirm that **up** deletes only that definition in xyOps.
242
+ 6. For both Git-initiated tests, confirm that the sync API Key's activity did not launch either System Activity Event. A second downsync job means the key is missing from a trigger's exclusion list.
240
243
 
241
244
  Before the first live delete, preview each direction against the complete checkout and inspect every proposed deletion:
242
245
 
package/index.js CHANGED
@@ -14,6 +14,16 @@ const Uncatch = require('uncatch');
14
14
  const cli = require('pixl-cli');
15
15
  const pkg = require('./package.json');
16
16
 
17
+ // MCP owns its streams and lifecycle. Enter before pixl-cli installs terminal
18
+ // output, signal handlers, or argument expansion that could consume stdin.
19
+ if (process.argv[2] === 'mcp') {
20
+ require('./lib/mcp.js').main(process.argv.slice(3)).catch(function(err) {
21
+ process.stderr.write('xy mcp: ' + err.message + '\n');
22
+ process.exitCode = 1;
23
+ });
24
+ return;
25
+ }
26
+
17
27
  if (!process.env.XYOPS_USER_AGENT) {
18
28
  process.env.XYOPS_USER_AGENT = `xyOps CLI v${pkg.version} (Node.js); ${os.hostname()} (${os.platform()}/${os.arch()})`;
19
29
  }
@@ -79,7 +89,7 @@ const TOP_LEVEL_COMMANDS = [
79
89
  'plugins', 'plugin', 'servers', 'server', 'secrets', 'secret',
80
90
  'snapshots', 'snapshot', 'tags', 'tag', 'tickets', 'ticket',
81
91
  'hooks', 'hook', 'marketplace',
82
- 'import', 'api', 'repl'
92
+ 'import', 'api', 'repl', 'mcp'
83
93
  ];
84
94
 
85
95
  // coerce true/false into booleans
package/lib/config.js CHANGED
@@ -42,7 +42,12 @@ var config_layer = module.exports = {
42
42
  if (ekey.match(/^XYOPS_(\w+)$/)) {
43
43
  var key = RegExp.$1.toLowerCase();
44
44
  var value = process.env[ekey];
45
- if (value === "true") value = true;
45
+ // Parse object and array values just like JSON-looking CLI arguments.
46
+ if (value.trim().match(/^\{[\s\S]*\}$/) || value.trim().match(/^\[[\s\S]*\]$/)) {
47
+ try { value = JSON.parse(value); }
48
+ catch (err) { throw new Error('Failed to parse JSON from environment variable: ' + ekey); }
49
+ }
50
+ else if (value === "true") value = true;
46
51
  else if (value === "false") value = false;
47
52
  else if (String(value).match(/^\-?\d+(\.\d+)?$/)) value = parseFloat(value);
48
53
  else if (String(value).match(/^\-?\d+$/)) value = parseInt(value);
@@ -51,7 +56,7 @@ var config_layer = module.exports = {
51
56
  }
52
57
 
53
58
  for (var key in config) {
54
- process.env['XYOPS_' + key.toUpperCase()] = '' + config[key];
59
+ process.env['XYOPS_' + key.toUpperCase()] = (config[key] !== null && typeof config[key] === 'object') ? JSON.stringify(config[key]) : '' + config[key];
55
60
  }
56
61
 
57
62
  this.config = config;
@@ -0,0 +1,52 @@
1
+ // Shared SDK boundary. No terminal formatting, cached records, or global args.
2
+ const READ_APIS = new Set(['get_events', 'get_event', 'get_categories', 'get_groups', 'get_plugins', 'get_plugin', 'get_job', 'search_jobs', 'search_alerts', 'search_snapshots', 'get_active_servers']);
3
+ const WRITE_APIS = new Set(['create_event', 'update_event', 'delete_event', 'run_event', 'create_ticket', 'update_ticket', 'delete_ticket', 'add_ticket_change', 'create_snapshot', 'create_group_snapshot']);
4
+
5
+ function checkKeys(value) {
6
+ // Prevent prototype-bearing keys from reaching downstream merge helpers.
7
+ if (!value || typeof value !== 'object') return;
8
+ for (const key of Object.keys(value)) {
9
+ if (['__proto__', 'prototype', 'constructor'].includes(key)) throw new Error('Unsupported property: ' + key);
10
+ checkKeys(value[key]);
11
+ }
12
+ }
13
+
14
+ function createContext(api, options = {}) {
15
+ const limits = Object.freeze({ max_page_size: options.max_page_size || 100, max_result_bytes: options.max_result_bytes || 1048576 });
16
+ const redact = text => String(text).split(options.api_key || '\0').join('[redacted]');
17
+ const log = message => process.stderr.write('xy mcp: ' + redact(message) + '\n');
18
+
19
+ return {
20
+ limits, log, redact, readOnly: !!options.read_only,
21
+ withApiKey(api_key) {
22
+ // HTTP requests get their own immutable credential scope, including
23
+ // error redaction. Never change environment variables or SDK defaults.
24
+ if (typeof api_key !== 'string' || !api_key) throw new Error('Missing xyOps API key.');
25
+ return createContext(api, { ...options, api_key });
26
+ },
27
+ async callAPI(name, params = {}, signal) {
28
+ if (!READ_APIS.has(name) && !WRITE_APIS.has(name)) throw new Error('Unsupported API: ' + name);
29
+ if (options.read_only && !READ_APIS.has(name)) throw new Error('MCP is configured read-only.');
30
+ if (signal?.aborted) throw new Error('Request cancelled before contacting xyOps.');
31
+ checkKeys(params);
32
+
33
+ // Retry policy belongs to this boundary, not the AI client. Retrying a
34
+ // timed-out launch could create a second job. The SDK also starts with
35
+ // retries disabled in the MCP entry point.
36
+ let response;
37
+ const requestOptions = { retries: 0 };
38
+ if (options.api_key) requestOptions.headers = { 'X-API-Key': options.api_key };
39
+ try { response = await api[name](params, requestOptions); }
40
+ catch (err) { response = { err }; }
41
+ if (response?.err || !response?.data || response.data.code) {
42
+ const definitive = response?.data && response.data.code;
43
+ const message = definitive ? response.data.description : (response?.err?.message || response?.err || 'Invalid xyOps response');
44
+ const uncertain = WRITE_APIS.has(name) && !definitive;
45
+ throw new Error(redact(message) + (uncertain ? ' Outcome is uncertain; inspect xyOps before retrying this operation.' : ''));
46
+ }
47
+ return response.data;
48
+ }
49
+ };
50
+ }
51
+
52
+ module.exports = { createContext, checkKeys };
@@ -0,0 +1,27 @@
1
+ // Load only the bundled tools. Helpers belong outside the tools directory.
2
+ const fs = require('node:fs');
3
+ const Path = require('node:path');
4
+ const { fromJsonSchema } = require('@modelcontextprotocol/server');
5
+
6
+ module.exports = function loadTools(directory = Path.join(__dirname, 'tools')) {
7
+ const tools = new Map();
8
+
9
+ for (const file of fs.readdirSync(directory).filter(file => file.endsWith('.js')).sort()) {
10
+ const tool = require(Path.join(directory, file));
11
+ if (typeof tool.describe !== 'function' || typeof tool.run !== 'function') {
12
+ throw new Error('Invalid MCP tool exports: ' + file);
13
+ }
14
+ const description = tool.describe();
15
+ if (!description || !/^[a-z][a-z0-9_]*$/.test(description.name) || !description.description ||
16
+ !description.inputSchema || description.inputSchema.type !== 'object' ||
17
+ typeof description.annotations?.readOnlyHint !== 'boolean') {
18
+ throw new Error('Invalid MCP tool description: ' + file);
19
+ }
20
+ if (tools.has(description.name)) throw new Error('Duplicate MCP tool: ' + description.name);
21
+
22
+ // Compile once, rather than rebuilding validators for each HTTP request.
23
+ const inputSchema = fromJsonSchema(description.inputSchema);
24
+ tools.set(description.name, { ...tool, description, inputSchema });
25
+ }
26
+ return tools;
27
+ };
@@ -0,0 +1,14 @@
1
+ // Tools return ordinary objects. Only this layer knows the MCP result envelope.
2
+ function result(data, limit) {
3
+ const text = JSON.stringify(data);
4
+ if (Buffer.byteLength(text) > limit) {
5
+ throw new Error('Result exceeds the MCP output limit. Request fewer rows or selected fields. The operation may already have completed.');
6
+ }
7
+ return { structuredContent: data, content: [{ type: 'text', text }] };
8
+ }
9
+
10
+ function failure(error, redact) {
11
+ return { isError: true, content: [{ type: 'text', text: redact(error.message || error) }] };
12
+ }
13
+
14
+ module.exports = { result, failure };