From 1e74e4314c8bfc3bce9379692c6e5452c4a99535 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 25 Feb 2026 10:37:39 +0000 Subject: [PATCH] Removes Ably MCP and positions Ably CLI as Public Preview --- .cursor/rules/AI-Assistance.mdc | 1 - .cursor/rules/Development.mdc | 2 +- .cursor/rules/Project.mdc | 9 +- .cursor/rules/Workflow.mdc | 3 +- .nycrc.json | 1 - CONTRIBUTING.md | 1 - README.md | 127 +- docs/Product-Requirements.md | 377 ----- docs/Project-Structure.md | 11 +- docs/TODO.md | 251 ---- docs/workplans/README.md | 4 +- eslint.config.js | 7 - package.json | 1 - pnpm-lock.yaml | 422 +----- src/base-command.ts | 10 - src/commands/mcp/index.ts | 11 - src/commands/mcp/start-server.ts | 43 - src/help.ts | 4 +- src/mcp/index.ts | 1 - src/mcp/mcp-server.ts | 1291 ----------------- src/types/modelcontextprotocol.d.ts | 39 - test/integration/interactive-mode.test.ts | 2 +- test/unit/base/base-command-enhanced.test.ts | 4 - test/unit/base/base-command.test.ts | 12 - .../commands/interactive-autocomplete.test.ts | 10 - test/unit/commands/mcp/mcp.test.ts | 227 --- test/unit/help/interactive-help.test.ts | 1 - test/unit/help/web-cli-help.test.ts | 5 +- 28 files changed, 57 insertions(+), 2820 deletions(-) delete mode 100644 docs/Product-Requirements.md delete mode 100644 docs/TODO.md delete mode 100644 src/commands/mcp/index.ts delete mode 100644 src/commands/mcp/start-server.ts delete mode 100644 src/mcp/index.ts delete mode 100644 src/mcp/mcp-server.ts delete mode 100644 src/types/modelcontextprotocol.d.ts delete mode 100644 test/unit/commands/mcp/mcp.test.ts diff --git a/.cursor/rules/AI-Assistance.mdc b/.cursor/rules/AI-Assistance.mdc index acb7b11a..7b71bbc7 100644 --- a/.cursor/rules/AI-Assistance.mdc +++ b/.cursor/rules/AI-Assistance.mdc @@ -12,7 +12,6 @@ This document provides guidance for AI assistants working with the Ably CLI code ### Understanding the Project 1. **Start with Key Documentation**: - - [Product Requirements](mdc:docs/Product-Requirements.md) - [Project Structure](mdc:docs/Project-Structure.md) - [Testing Strategy](mdc:docs/Testing.md) - [Mandatory Workflow](mdc:.cursor/rules/Workflow.mdc) diff --git a/.cursor/rules/Development.mdc b/.cursor/rules/Development.mdc index 7a1ebdaa..b7fef5b5 100644 --- a/.cursor/rules/Development.mdc +++ b/.cursor/rules/Development.mdc @@ -18,7 +18,7 @@ alwaysApply: true 3. **Run Tests:** Execute relevant tests locally (e.g., using `pnpm test`) to verify that your changes haven't introduced regressions. Ensure all affected tests pass. For new features, add appropriate unit, integration, or end-to-end tests. 4. **Update Documentation:** Update all relevant documentation, including: * `README.md` (especially the command list if commands were changed/added). - * Files within the `docs/` directory (`Product-Requirements.md`, `Project-Structure.md`, `Testing.md` etc.) if the changes affect requirements, structure, or testing procedures. + * Files within the `docs/` directory (`Project-Structure.md`, `Testing.md` etc.) if the changes affect structure or testing procedures. * Files within the `.cursor/rules/` directory if the changes impact development guidelines, Ably usage, or project structure. **Failure to complete these steps means the work is not finished.** diff --git a/.cursor/rules/Project.mdc b/.cursor/rules/Project.mdc index 5b172b5e..19b0b20f 100644 --- a/.cursor/rules/Project.mdc +++ b/.cursor/rules/Project.mdc @@ -7,10 +7,5 @@ alwaysApply: true The following documents must be read and considered before making any changes to this project. -1. [Product-Requirements.md](mdc:docs/Product-Requirements.md) - outlines the project's goals, features, and technical specifications, acting as the primary blueprint for the Ably CLI's development. -2. [Testing.md](mdc:docs/Testing.md) - details the strategy and approach for testing the CLI, ensuring its quality, reliability, and adherence to the defined requirements. -3. [Project-Structure.md](mdc:docs/Project-Structure.md) - provides an overview of the project's structure and purpose of each folder/file. - -## Outstanding tasks - -1. [TODO.md](mdc:docs/TODO.md) contains a list of outstanding tasks for this project. Note this list is not exhaustive as tasks may exist in Github issues or in JIRA, but this should give you some visibility into what is not yet done and will be done at some point in the future. Do not pick up any of these TODO tasks unless explicitly advised to. +1. [Testing.md](mdc:docs/Testing.md) - details the strategy and approach for testing the CLI, ensuring its quality, reliability, and adherence to the defined requirements. +2. [Project-Structure.md](mdc:docs/Project-Structure.md) - provides an overview of the project's structure and purpose of each folder/file. diff --git a/.cursor/rules/Workflow.mdc b/.cursor/rules/Workflow.mdc index 563773a8..3e892eb6 100644 --- a/.cursor/rules/Workflow.mdc +++ b/.cursor/rules/Workflow.mdc @@ -26,9 +26,8 @@ alwaysApply: true * Review and update all relevant documentation and rule files based on your changes. * **Checklist:** * `README.md`: Especially the command list if commands were added/changed (often handled by `pnpm prepare`). - * `docs/*.md`: Any file impacted by the changes (e.g., `Product-Requirements.md`, `Project-Structure.md`, `Testing.md`). + * `docs/*.md`: Any file impacted by the changes (e.g., `Project-Structure.md`, `Testing.md`). * `.cursor/rules/*.mdc`: Any rule file impacted by changes to development practices, Ably usage, project structure, etc. - * `docs/TODO.md`: Update any related tasks. * **Purpose:** Keeps project knowledge current for both humans and AI agents. * **Verification:** Manually confirm that documentation accurately reflects the implemented changes. diff --git a/.nycrc.json b/.nycrc.json index 3006aaa7..197cf841 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -6,7 +6,6 @@ "exclude": [ "src/**/*.d.ts", "src/scripts/**/*", - "src/mcp/**/*", "dist/**/*", "test/**/*", "coverage/**/*", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb7a8be6..18e539e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,6 @@ In summary, this involves: Before starting work, please familiarize yourself with: -* [Product Requirements](./docs/Product-Requirements.md): Understand the goals and features. * [Project Structure](./docs/Project-Structure.md): Know where different code components live. * [Testing Strategy](./docs/Testing.md): Understand the different types of tests and how to run them. * [Development Rules](mdc:.cursor/rules/Development.mdc): Coding standards, linting, dependency management. diff --git a/README.md b/README.md index ab9bf060..a7b71a2f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,23 @@ -# Ably CLI and MCP server +# Ably CLI [![npm version](https://badge.fury.io/js/@ably%2Fcli.svg)](https://badge.fury.io/js/@ably%2Fcli) -[Ably](https://ably.com) CLI and MCP server for [Ably Pub/Sub](https://ably.com/pubsub), [Ably Spaces](https://ably.com/spaces), [Ably Chat](https://ably.com/chat) and the [Ably Control API](https://ably.com/docs/account/control-api). - -> [!NOTE] -> This project is in beta and this CLI and MCP server project is being actively developed. -> Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback, feature requests or want to report a bug. We welcome [pull requests too](https://github.com/ably/ably-cli/pulls). +[Ably](https://ably.com) CLI for [Ably Pub/Sub](https://ably.com/pubsub), [Ably Spaces](https://ably.com/spaces), [Ably Chat](https://ably.com/chat) and the [Ably Control API](https://ably.com/docs/account/control-api). ![Ably CLI screenshot](assets/cli-screenshot.png) -* [Ably CLI and MCP server](#ably-cli-and-mcp-server) +* [Ably CLI](#ably-cli) * [CLI Usage](#cli-usage) -* [MCP Usage](#mcp-usage) * [Commands](#commands) -* [MCP Server](#mcp-server) * [Contributing](#contributing) -* [or](#or) # CLI Usage +> [!NOTE] +> The Ably CLI is currently in Public Preview status. Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback, feature requests or want to report a bug. + ```sh-session $ npm install -g @ably/cli @@ -71,20 +67,6 @@ $ ably-interactive - Double Ctrl+C (within 500ms) force quits the shell - **No "ably" prefix needed**: Commands can be typed directly (e.g., just `channels list` instead of `ably channels list`) -# MCP Usage - -> [!WARNING] -> The MCP server is currently experimental. Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback or suggestions for features. - -1. Install the CLI following the [CLI usage](#cli-usage) steps. -2. Follow the instructions for your tool to set up an MCP server, such as [Claude desktop](https://modelcontextprotocol.io/quickstart/user), and configure: - 1. `command` as ably mcp start-server - -See [MCP Server section](#mcp-server) for more details on how to use the MCP Server. - -> [!NOTE] -> If you are having trouble getting the MCP server running, use [MCP inspector](https://github.com/modelcontextprotocol/inspector) - # Commands @@ -162,8 +144,6 @@ See [MCP Server section](#mcp-server) for more details on how to use the MCP Ser * [`ably logs push history`](#ably-logs-push-history) * [`ably logs push subscribe`](#ably-logs-push-subscribe) * [`ably logs subscribe`](#ably-logs-subscribe) -* [`ably mcp`](#ably-mcp) -* [`ably mcp start-server`](#ably-mcp-start-server) * [`ably queues`](#ably-queues) * [`ably queues create`](#ably-queues-create) * [`ably queues delete QUEUEID`](#ably-queues-delete-queueid) @@ -3111,43 +3091,6 @@ EXAMPLES _See code: [src/commands/logs/subscribe.ts](https://github.com/ably/ably-cli/blob/v0.16.0/src/commands/logs/subscribe.ts)_ -## `ably mcp` - -Experimental Model Context Protocol (MCP) commands for AI tools to interact with Ably - -``` -USAGE - $ ably mcp - -DESCRIPTION - Experimental Model Context Protocol (MCP) commands for AI tools to interact with Ably - -EXAMPLES - $ ably mcp start-server - -COMMANDS - ably mcp start-server Start an MCP server for AI tools to interact with Ably (currently experimental) -``` - -_See code: [src/commands/mcp/index.ts](https://github.com/ably/ably-cli/blob/v0.16.0/src/commands/mcp/index.ts)_ - -## `ably mcp start-server` - -Start an MCP server for AI tools to interact with Ably (currently experimental) - -``` -USAGE - $ ably mcp start-server - -DESCRIPTION - Start an MCP server for AI tools to interact with Ably (currently experimental) - -EXAMPLES - $ ably mcp start-server -``` - -_See code: [src/commands/mcp/start-server.ts](https://github.com/ably/ably-cli/blob/v0.16.0/src/commands/mcp/start-server.ts)_ - ## `ably queues` Manage Ably Queues @@ -4866,62 +4809,6 @@ EXAMPLES _See code: [src/commands/support/contact.ts](https://github.com/ably/ably-cli/blob/v0.16.0/src/commands/support/contact.ts)_ -# MCP Server - -The Ably CLI can also act as an MCP (Model Context Protocol) server for AI use cases, specifically focussed on IDE tools and AI desktop tools like Claude that support MCP. - -## Using the MCP Server - -To start the MCP server, run: - -```sh -ably-mcp -``` - -Or via npm/pnpm script: - -```sh -pnpm mcp-server -``` - -The MCP server runs in file mode (stdio transport) and exposes a subset of Ably CLI commands that AI tools can use to interact with Ably channels. - -## Environment Variables - -The MCP server supports the following environment variables for authentication and configuration: - -- `ABLY_ACCESS_TOKEN` - Overrides the default access token used for the Control API -- `ABLY_API_KEY` - Overrides the default API key used for the data plane -- `ABLY_CLIENT_ID` - Overrides the default client ID assigned -- `ABLY_CONTROL_HOST` - Overrides the default control API host -- `ABLY_HOST` - Overrides the default data plane host -- `ABLY_ENVIRONMENT` - Overrides the default data plane environment - -### Update Notification Environment Variables - -The CLI also supports environment variables to control update notifications: - -- `ABLY_SKIP_NEW_VERSION_CHECK=1` - Skip automatic version update checks -- `ABLY_FORCE_VERSION_CACHE_UPDATE=1` - Force an immediate version check - -## Available MCP Commands - -The MCP server exposes the following subset of Ably CLI commands: - -- `list_channels` - List active channels using the channel enumeration API -- `subscribe_to_channel` - Subscribe to messages on an Ably channel -- `publish_to_channel` - Publish a message to an Ably channel -- `get_channel_history` - Retrieve message history for a channel -- Channel Presence - Access presence information for channels - -## MCP Resources - -The MCP server also provides the following resources: - -- `channels` - List of active channels on the Ably platform -- `channel_history` - Historical messages from a specific Ably channel -- `channel_presence` - Current presence members on a specific Ably channel - # Contributing Please see the documentation in [`.cursor/rules/Workflow.mdc`](.cursor/rules/Workflow.mdc) for details on how to contribute to this project, including our mandatory development workflow, testing requirements, and code quality standards. @@ -4929,7 +4816,7 @@ Please see the documentation in [`.cursor/rules/Workflow.mdc`](.cursor/rules/Wor ## For AI Assistants **IMPORTANT**: See [`.claude/CLAUDE.md`](./.claude/CLAUDE.md) for mandatory instructions before making any changes. -If you are an AI assistant, start with [`AI_START_HERE.md`](./AI_START_HERE.md) first. +If you are an AI assistant, start with [`AI-Assistance.mdc`](.cursor/rules/AI-Assistance.mdc) first. ## Quick Development Validation diff --git a/docs/Product-Requirements.md b/docs/Product-Requirements.md deleted file mode 100644 index 98130793..00000000 --- a/docs/Product-Requirements.md +++ /dev/null @@ -1,377 +0,0 @@ -# Product Requirements Document: Ably CLI - -## 1. Overview - -The Ably CLI enables developers to integrate, test, and debug Ably's product features directly from the command line. It also allows managing Ably apps and accounts via the Control API. - -## 2. Objective - -Provide an intuitive CLI for developers to interact with Ably APIs and SDKs, aiding discovery, initial development, and production use. The CLI should be easily extensible for new product features and persist authentication credentials securely. - -## 3. Target Audience - -- Software developers using Ably. -- DevOps engineers managing Ably resources. - -## 4. Control Plane vs. Data Plane - -- **Control Plane**: Manages configuration, administration, setup, and lifecycle of apps, resources, and accounts via the Ably dashboard and Control API (). -- **Data Plane**: Handles core product functionality (Pub/Sub, Chat, Spaces) via SDKs and REST APIs (). - -## 5. Key Features & Functional Requirements - -### 5.1. Command Hierarchy - -The CLI uses topics (space-separated) to group related commands logically. - -*Note: The `topicSeparator` configured for oclif is a space (` `). All commands must respect this configuration.* - -**Account Management (`ably accounts`)** -*(Requires Control API access token)* - -- `$ ably login [TOKEN]`: Alias for `ably accounts login`. Logs in or prompts for token. -- `$ ably accounts login [TOKEN]`: Authenticates the user with a Control API access token, storing it in the local config. Supports `--alias` for multiple accounts. -- `$ ably accounts list`: Lists locally configured accounts (aliases, token details, user info, account details). -- `$ ably accounts logout [ALIAS]`: Removes the access token and associated data for the specified (or default) account after warning. -- `$ ably accounts current`: Shows the currently selected account (alias or `default`) and verifies the token against the Control API `/me` endpoint. -- `$ ably accounts stats`: Views account stats. Supports `--live` for polling key metrics (peak connections, channels, message throughput, etc.). -- `$ ably accounts switch [ALIAS]`: Switches the active account configuration to the specified alias or prompts for selection. - -**App Management (`ably apps`)** -*(Requires Control API access token; operations scoped to the current or specified app)* - -- `$ ably apps list`: Lists all apps available in the current account. -- `$ ably apps create`: Creates a new app. Requires `--name`. Supports `--tls-only`. -- `$ ably apps update ID`: Updates the app specified by `ID`. Supports `--name`, `--tls-only`. -- `$ ably apps delete [ID]`: Deletes the specified (or current) app after confirmation. Supports `--force`. -- `$ ably apps set-apns-p12 ID`: Uploads an APNS P12 certificate for the specified app `ID`. Requires `--certificate`. Supports `--password`, `--use-for-sandbox`. -- `$ ably apps stats [ID]`: Views app stats for the specified (or current) app. Supports `--live` polling, `--unit`, `--start`, `--end`, `--limit`. -- `$ ably apps switch [APPID]`: Switches the active app configuration to the specified App ID or prompts for selection. -- `$ ably apps current`: Shows the currently selected app configuration. - -**Channel Rules (`ably apps channel-rules`)** -*(Manage Ably channel rules/namespaces via Control API)* - -- `$ ably apps channel-rules list`: Lists all channel rules for the current/specified app. -- `$ ably apps channel-rules create`: Creates a channel rule. Requires `--name`. Supports various flags like `--persisted`, `--push-enabled`, etc. -- `$ ably apps channel-rules update NAMEORID`: Updates a channel rule specified by name or ID. Supports various flags. -- `$ ably apps channel-rules delete NAMEORID`: Deletes a channel rule specified by name or ID after confirmation. Supports `--force`. - -**Authentication & Authorization (`ably auth`)** -*(Manage data plane auth: API Keys, Tokens)* - -- `$ ably auth issue-jwt-token`: Creates an Ably JWT token. Supports `--capability`, `--ttl`, `--client-id`, `--token-only`. -- `$ ably auth issue-ably-token`: Creates an Ably Token. Supports `--capability`, `--ttl`, `--client-id`, `--token-only`. -- `$ ably auth revoke-token TOKEN`: Revokes a specific Ably Token or JWT. Supports revoking by `--client-id`. - -**API Key Management (`ably auth keys`)** -*(Manage API keys via Control API)* - -- `$ ably auth keys list`: Lists all API keys for the current/specified app. -- `$ ably auth keys create`: Creates a new API key. Requires `--name`. Supports `--capabilities`. -- `$ ably auth keys get KEYNAMEORVALUE`: Shows details for a specific API key (using `APP_ID.KEY_ID` format or full key value). -- `$ ably auth keys update KEYNAME`: Updates properties (name, capabilities) of an API key (using `APP_ID.KEY_ID` format). -- `$ ably auth keys revoke KEYNAME`: Revokes an API key (using `APP_ID.KEY_ID` format) after confirmation. Supports `--force`. -- `$ ably auth keys switch [KEYNAMEORVALUE]`: Sets the default API key for the current app in the local config. Prompts if no key specified. -- `$ ably auth keys current`: Shows the currently configured API key for the selected app. - -**Pub/Sub Channels (`ably channels`)** -*(Interact with Ably Pub/Sub using the Realtime SDK)* - -- `$ ably channels list`: Lists active channels via channel enumeration API. Supports `--prefix`, `--limit`. -- `$ ably channels publish CHANNEL MESSAGE`: Publishes a message. Supports `--name`, `--encoding`, `--count`, `--delay`, JSON/text message, message interpolation (`{{.Count}}`, `{{.Timestamp}}`), `--transport rest|realtime`. -- `$ ably channels batch-publish [MESSAGE]`: Publishes multiple messages via REST batch API. Supports `--channels`, `--channels-json`, `--spec`. -- `$ ably channels subscribe CHANNELS...`: Subscribes to messages on one or more channels. Supports `--rewind`, `--delta`, `--cipher-*` flags for decryption. Runs until terminated. -- `$ ably channels history CHANNEL`: Retrieves message history. Supports `--start`, `--end`, `--limit`, `--direction`, `--cipher` flags. -- `$ ably channels logs [TOPIC]`: Alias for `ably logs channel-lifecycle subscribe`. (Currently only supports `channel-lifecycle`). -- `$ ably channels occupancy get CHANNEL`: Gets current occupancy metrics for a channel. -- `$ ably channels occupancy subscribe CHANNEL`: Subscribes to live occupancy metrics using the meta channel. Runs until terminated. -- `$ ably channels presence enter CHANNEL`: Enters presence and stays present. Supports `--data`, `--show-others`. Runs until terminated. -- `$ ably channels presence subscribe CHANNEL`: Subscribes to presence events (joins, leaves, updates). Runs until terminated. - -**Connections (`ably connections`)** -*(Interact with Pub/Sub connections)* - -- `$ ably connections stats`: Shows connection stats (similar to `ably apps stats` but connection-focused). Supports `--live`. -- `$ ably connections test`: Performs a simple connection test. Supports transport options. -- `$ ably connections logs [TOPIC]`: Alias for `ably logs connection-lifecycle subscribe`. (Currently only supports `connection-lifecycle`). - -**Chat Rooms (`ably rooms`)** -*(Interact with Ably Chat using the Chat SDK)* - -- `$ ably rooms list`: Lists chat rooms (filters channel enumeration). -- `$ ably rooms messages send ROOMID TEXT`: Sends a chat message. Supports `--count`, `--delay`, interpolation. -- `$ ably rooms messages subscribe ROOMID`: Subscribes to chat messages. Runs until terminated. -- `$ ably rooms messages history ROOMID`: Gets historical chat messages. -- `$ ably rooms occupancy get ROOMID`: Gets current occupancy for a room. -- `$ ably rooms occupancy subscribe ROOMID`: Subscribes to live room occupancy. Runs until terminated. -- `$ ably rooms presence enter ROOMID`: Enters presence in a room and stays present. Runs until terminated. -- `$ ably rooms presence subscribe ROOMID`: Subscribes to room presence events. Runs until terminated. -- `$ ably rooms reactions send ROOMID MESSAGEID EMOJI`: Sends a message reaction. -- `$ ably rooms reactions subscribe ROOMID`: Subscribes to message reactions. Runs until terminated. -- `$ ably rooms typing keystroke ROOMID`: Starts sending typing indicators. Runs until terminated. -- `$ ably rooms typing subscribe ROOMID`: Subscribes to typing indicators. Runs until terminated. - -**Spaces (`ably spaces`)** -*(Interact with Ably Spaces using the Spaces SDK)* - -- `$ ably spaces list`: Lists Spaces (filters channel enumeration). -- `$ ably spaces members enter SPACEID`: Enters a Space and stays present. Runs until terminated. -- `$ ably spaces members subscribe SPACEID`: Subscribes to Space member events (enter, leave, update). Runs until terminated. -- `$ ably spaces locations set SPACEID`: Sets own location and stays updated. Shows other location changes. Runs until terminated. Supports position arguments. -- `$ ably spaces locations subscribe SPACEID`: Subscribes to location updates for all members. Runs until terminated. -- `$ ably spaces locations get-all SPACEID`: Gets current locations for all members and exits. -- `$ ably spaces cursors set SPACEID`: Sets own cursor position. Simulates movement if no position given. Runs until terminated. Supports position arguments. -- `$ ably spaces cursors subscribe SPACEID`: Subscribes to cursor position updates. Uses color coding for changes. Runs until terminated. -- `$ ably spaces cursors get-all SPACEID`: Gets current cursor positions for all members and exits. -- `$ ably spaces locks acquire SPACEID LOCKID`: Acquires a lock and holds it. Shows other lock changes. Runs until terminated. -- `$ ably spaces locks get SPACEID LOCKID`: Checks if a specific lock exists and shows details. -- `$ ably spaces locks get-all SPACEID`: Gets all current locks and exits. -- `$ ably spaces locks subscribe SPACEID`: Subscribes to lock status updates. Uses color coding for changes. Runs until terminated. - -**Logging (`ably logs`)** -*(Stream and retrieve logs from meta channels)* - -- `$ ably logs subscribe`: Streams logs from `[meta]log`. Supports `--rewind`. -- `$ ably logs history`: Retrieves historical logs from `[meta]log`. Supports `--limit`, `--direction`. -- `$ ably logs channel-lifecycle subscribe`: Streams logs from `[meta]channel.lifecycle`. -- `$ ably logs connection-lifecycle subscribe`: Streams logs from `[meta]connection.lifecycle`. -- `$ ably logs connection-lifecycle history`: Retrieves historical connection logs. Supports `--limit`, `--direction`. -- `$ ably logs connection subscribe`: Streams logs from `[meta]connection.lifecycle`. Supports `--rewind`. -- `$ ably logs push subscribe`: Streams logs from `[meta]log:push`. Supports `--rewind`. -- `$ ably logs push history`: Retrieves historical push logs from `[meta]log:push`. Supports `--limit`, `--direction`. - -**Integrations (`ably integrations`)** -*(Manage Ably integrations/rules via Control API)* - -- `$ ably integrations list`: Lists all integration rules for the current/specified app. -- `$ ably integrations create`: Creates an integration rule. Requires various flags depending on rule type. -- `$ ably integrations get RULEID`: Shows details for a specific integration rule. -- `$ ably integrations update RULEID`: Updates an integration rule. Supports various flags. -- `$ ably integrations delete RULEID`: Deletes an integration rule after confirmation. Supports `--force`. - -**Queues (`ably queues`)** -*(Manage Ably Queues via Control API)* - -- `$ ably queues list`: Lists all queues for the current/specified app. -- `$ ably queues create`: Creates a queue. Requires various flags. -- `$ ably queues delete QUEUENAME`: Deletes a queue after confirmation. Supports `--force`. - -**Benchmarking (`ably bench`)** -*(Run benchmark tests)* - -- `$ ably bench publisher CHANNEL`: Starts a publisher test. Measures latency, throughput. Waits for subscribers if `--wait-for-subscribers` is used. Shows progress UI. Supports `--messages`, `--rate`, `--message-size`, `--transport rest|realtime`. -- `$ ably bench subscriber CHANNEL`: Starts a subscriber test. Waits for publisher, measures latency, message count. Shows progress UI. Runs until publisher finishes, then waits for next test. - -**Help & Info (`ably help`)** - -- `$ ably help ask QUESTION`: Sends a question to the Ably AI Help agent via Control API. Shows "Thinking..." animation. Displays answer and links. Supports `--continue` for follow-up questions, storing conversation context locally. -- `$ ably help contact`: Opens in the browser. -- `$ ably help support`: Opens in the browser. -- `$ ably help status`: Checks Ably service status via and directs user to . -- `$ ably --version`: Shows the CLI version. -- `$ ably [topic]`: If a topic is called without a command, it shows available sub-commands, descriptions, and examples. -- `$ ably [command] --help`: Shows detailed help for a specific command. - -**Configuration (`ably config`)** - -- `$ ably config`: Opens the local config file (`~/.ably/config`) in the default text editor. Supports `--editor`. - -**MCP Server (`ably mcp`)** - -- `$ ably mcp start-server`: Starts the CLI in Model Context Protocol (MCP) server mode. - -### 5.2. Authentication - -Two types: -1. **Control Plane (User Level)**: Uses Control API access tokens obtained from . Scoped to one user and one account. Managed via `ably accounts` commands. -2. **Data Plane (App Level)**: Uses API keys or Ably Tokens/JWTs. Scoped to a single app. Managed via `ably auth` commands. The CLI prioritizes credentials in this order: - * Command-line flags (`--api-key`, `--token`, `--access-token`). - * Environment variables (`ABLY_API_KEY`, `ABLY_TOKEN`, `ABLY_ACCESS_TOKEN`). - * Locally stored configuration (`~/.ably/config`) for the current app/account. - -**Convenience Workflow:** -If a data plane command is run without explicit auth flags/env vars and no app/key is configured locally: -- If logged in (`ably accounts login` done), the CLI prompts the user to select an app from their account. -- Then, it prompts the user to select an API key for that app. -- The selected app ID and API key are stored in the local config for future use with that app under the current account profile. - -**Login Flow (`ably accounts login`):** -- Displays Ably ASCII art. -- Prompts user to press Enter to open in browser (unless `--no-browser`). -- Prompts user to paste the obtained token. -- Prompts for an optional alias (`--alias`) to store the account credentials. If no alias, updates the `default` account profile. - -### 5.3. Configuration (`~/.ably/config`) - -- Stored in TOML format at `~/.ably/config`. -- File permissions should be secured (e.g., `600`). -- Stores Control API access tokens (potentially multiple, identified by `default` or alias). -- Stores the currently active account alias. -- For each account profile, stores the currently active app ID. -- For each account profile, stores a mapping of app IDs to their last used API key. -- `ably config` command opens this file. - -### 5.4. Global Arguments - -- `--host `: Overrides the default REST/Realtime host for Data Plane SDK calls. -- `--env `: Overrides the default environment (e.g., `production`) for Data Plane SDK calls. -- `--control-host `: Overrides the default Control API host (`control.ably.net`). -- `--access-token `: Overrides any configured Control API access token. -- `--api-key `: Overrides any configured Data Plane API key. -- `--token `: Authenticates Data Plane calls using an Ably Token or JWT instead of an API key. -- `--client-id `: Overrides the default client ID (`ably-cli-<8_random_chars>`) for Data Plane operations when using API key auth. Use `"none"` to disable sending a client ID. Not applicable for token auth. -- `--json`: Outputs results as raw, machine-readable JSON. Suppresses status messages and non-essential logs. Errors are output as JSON. -- `--pretty-json`: Outputs results as formatted, colorized JSON for human readability. Suppresses status messages. Errors are output as JSON. -- `--verbose` (`-v`): Outputs additional status/log events (e.g., connection state changes). Formatted as JSON when used with `--json` or `--pretty-json`. - -### 5.5. Usability & Developer Experience - -- Helpful inline documentation (`--help`). -- Autocompletion support for bash/zsh. -- Friendly, descriptive error messages with troubleshooting hints. -- Interactive prompts for confirmations (e.g., delete) and selections (e.g., switching accounts/apps without args). -- Use of console UI elements for better visualization (e.g., progress bars in `bench`, tables, status indicators). -- Did-you-mean suggestions for mistyped commands. -- Topic commands (`ably accounts`, `ably apps`, etc.) list sub-commands when called directly. - -### 5.6. Embeddable Web Example & React Component - -*(This section describes a feature allowing the CLI to run in a browser)* - -- **React Component (`@ably/react-cli-terminal` - hypothetical name)**: - - Embeddable component using Xterm.js (). - - Connects via WebSocket to a dedicated Terminal Server. - - Requires `controlAccessToken` and `apiKey` props passed during initialization. These map to `ABLY_ACCESS_TOKEN` and `ABLY_API_KEY` environment variables within the container. -- **Terminal Server**: - - Listens for WebSocket connections. - - On connection, spawns a new Docker container running the Ably CLI. - - Proxies WebSocket stream to the container's stdin/stdout. - - Terminates Docker container on WebSocket disconnect or after timeout (e.g., 30 mins). - - Limits concurrent sessions. - - Includes security measures (see [server/docker/README.md](../server/docker/README.md)). -- **Docker Container**: - - Based on `node:22-alpine` (or similar). - - Installs `@ably/cli`. - - Environment variables `ABLY_ACCESS_TOKEN` and `ABLY_API_KEY` must be set and validated on startup. - - Restricts execution to only the `ably` command for security. - - Sets an environment variable (e.g., `ABLY_CLI_MODE=web`) to indicate web context. -- **Web CLI Mode Restrictions**: - - The CLI detects `ABLY_CLI_MODE=web`. - - Disables commands related to local configuration and switching credentials: - - `ably accounts login`: Error message (already logged in). - - `ably accounts list`: Error message (use dashboard). - - `ably accounts logout`: Error message (cannot log out). - - `ably accounts switch`: Error message (use dashboard). - - `ably apps switch`: Error message (use dashboard). - - `ably auth keys switch`: Error message (use web interface). - - `ably config`: Error message (no local config). - - Adapts commands relying on local config: - - `ably accounts current`: Uses `/me` Control API endpoint based on provided token. - - `ably apps current`: Uses app ID from `ABLY_API_KEY` and calls Control API for details. -- **Demo Web Example (`/example/web-cli`)**: - - Simple Vite-based example showing how to use the React component. - - Reads `ABLY_ACCESS_TOKEN` and `ABLY_API_KEY` from `.env` file or environment. - - Includes usage instructions in `README.md`. - -### 5.6.1. Split-Screen Terminal Functionality (Optional) - -The `AblyCliTerminal` React component can optionally support a split-screen mode, allowing for two concurrent terminal sessions. - -- **Activation & UI**: - - A "split" icon is displayed on the terminal. Clicking it divides the view into two side-by-side terminal panes. - - When split, a tab bar appears above the panes, with each tab representing a session and including a close button. - - If one of two sessions is closed, the remaining session takes the full view, and the "split" icon reappears. - - Visual styling will be modern and dark-themed, with clear separation of elements. -- **Sessions & Authentication**: - - Supports a maximum of two independent sessions. - - Both sessions share the same `controlAccessToken` and `apiKey` provided to the parent component. -- **Connection Status**: - - The component's primary `onConnectionStatusChange` prop reflects the status of the left-most (or sole remaining) terminal. - - In-terminal visual status indicators (connecting, disconnected, errors) are displayed *within each respective terminal pane*. -- **Configuration**: - - Enabled via a new boolean prop (e.g., `enableSplitScreen`), defaulting to `false`. - - Documentation for this prop and its usage will be available in the component's `README.md`. -- **Resizable Panes (Optional Enhancement)**: - - The ability to drag a divider to resize panes is a potential future improvement. - -### 5.7. Model Context Protocol (MCP) Server - -*(This section describes running the CLI as an MCP server for AI tools)* - -- **Activation**: Run `$ ably mcp start-server`. -- **Technology**: Uses `@modelcontextprotocol/typescript-sdk` in file mode. -- **Authentication**: Relies on environment variables: - - `ABLY_ACCESS_TOKEN`: Control API token. - - `ABLY_API_KEY`: Data Plane key. - - `ABLY_CLIENT_ID`: Optional client ID override (defaults to `ably-mcp-`). - - `ABLY_CONTROL_HOST`: Optional Control API host override. - - `ABLY_HOST`: Optional Data Plane host override. - - `ABLY_ENVIRONMENT`: Optional Data Plane environment override. - - `ABLY_ENDPOINT`: Optional Data Plane endpoint override. -- **Behavior**: - - Disables all interactive prompts. - - Sets a maximum execution time per request (e.g., 15 seconds). Long-running commands like `subscribe` will run for this duration then exit gracefully. - - Uses a distinct client ID prefix (`ably-mcp-`). -- **Available MCP Commands (Subset of CLI)**: - - `ably apps list` - - `ably apps stats` - - `ably auth keys list` - - `ably channels history` - - `ably channels publish` - - `ably channels list` - - `ably channels presence get` (*Note: README has `ably channels presence subscribe` and `enter`, but not `get`. PRD lists `get` but not here. Assuming `get` is intended for MCP based on non-streaming nature.*) - -## 6. Technical Requirements - -- **Platforms**: Linux, macOS, Windows. -- **Technology**: Node.js, TypeScript, oclif framework (). -- **Distribution**: npm (`@ably/cli`), Homebrew, standalone executables. -- **Dependencies**: Use `pnpm`. Use SDKs (`ably-js`, `ably-chat-js`, `ably-spaces-js`) for data plane; direct HTTPS for Control API. Use JWT library for token generation. - -## 7. Testing Requirements - -- Unit, integration, and end-to-end tests are required. -- Data plane operations should ideally have E2E tests. -- Control API interactions can be mocked (based on OpenAPI spec). -- See [Testing Strategy](Testing.md) for strategy. - -## 8. API Dependencies - -- **Control API**: Used for account/app management. See and OpenAPI spec. Requires Control API access token. -- **Ably SDKs**: - - **Pub/Sub**: Use `ably-js` (). Use `request()` method for REST calls not covered by SDK methods. - - **Chat**: Use `ably-chat-js` (link TBC). Use underlying `ably-js` `request()` if needed. - - **Spaces**: Use `ably-spaces-js` (link TBC). Use underlying `ably-js` `request()` if needed. -- **JWT Library**: For generating Ably JWTs (`ably auth issue-jwt-token`). - -## 9. Usage Examples (Illustrative) - -```sh -# Log in (interactive or provide token) -ably login - -# List apps in the current account -ably apps list - -# Publish a message to a channel for the current/specified app -ably channels publish my-channel '{"greeting":"Hello"}' --name update - -# Subscribe to messages (runs until stopped) -ably channels subscribe my-channel --app "My Specific App" - -# Switch active account config -ably accounts switch my-work-alias - -# Ask the AI helper a question -ably help ask "How do I use channel history?" -``` - -## 10. Ably AI Agent (`ably help ask`) - -- Uses Control API endpoint `/v1/help`. -- POST request with `{"question": "user question"}`. -- If `--continue` flag is used, sends previous Q&A context along with the new question. -- Displays "Thinking..." animation while waiting. -- Renders Markdown answer and lists source links. -- Stores last Q&A locally for `--continue`. New question without flag resets context. diff --git a/docs/Project-Structure.md b/docs/Project-Structure.md index feb1dd3f..62160736 100644 --- a/docs/Project-Structure.md +++ b/docs/Project-Structure.md @@ -13,10 +13,8 @@ This document outlines the directory structure of the Ably CLI project. │ ├── run.cmd # Production run script (Windows). │ └── run.js # Production run script (Unix). ├── docs/ # Project documentation. -│ ├── Product-Requirements.md # Detailed product requirements. │ ├── Project-Structure.md # This file, outlining the project structure. -│ ├── Testing.md # Testing strategy and policy. -│ └── TODO.md # List of outstanding tasks. +│ └── Testing.md # Testing strategy and policy. ├── examples/ # Example usage of the CLI or related components. │ └── web-cli/ # Example implementation of the web-based CLI. ├── packages/ # Internal packages used by the project. @@ -40,7 +38,6 @@ This document outlines the directory structure of the Ably CLI project. │ │ ├── integrations/ # Commands for managing Ably integrations (rules). │ │ ├── login.ts # Alias command for `accounts login`. │ │ ├── logs/ # Commands for subscribing to various log streams. -│ │ ├── mcp/ # Commands specific to the MCP server functionality. │ │ ├── queues/ # Commands for managing Ably Queues. │ │ ├── rooms/ # Commands for interacting with Ably Chat rooms. │ │ └── spaces/ # Commands for interacting with Ably Spaces. @@ -50,9 +47,6 @@ This document outlines the directory structure of the Ably CLI project. │ │ ├── command_not_found/ # Hook for handling unknown commands. │ │ └── init/ # Hook executed at CLI initialization. │ ├── index.ts # Main entry point for the CLI source. -│ ├── mcp/ # Code related to the Model Context Protocol (MCP) server. -│ │ ├── index.ts # Entry point for MCP functionality. -│ │ └── mcp-server.ts # Implementation of the MCP server. │ ├── services/ # Core services used across commands. │ │ ├── config-manager.ts # Service for managing CLI configuration. │ │ ├── control-api.ts # Service for interacting with the Ably Control API. @@ -60,8 +54,7 @@ This document outlines the directory structure of the Ably CLI project. │ │ └── stats-display.ts # Service for displaying stats information. │ ├── spaces-base-command.ts # Base class specific to Ably Spaces commands. │ ├── types/ # TypeScript type definitions. -│ │ ├── cli.ts # General CLI type definitions. -│ │ └── modelcontextprotocol.d.ts # Type definitions for MCP. +│ │ └── cli.ts # General CLI type definitions. │ └── utils/ # Utility functions. │ ├── json-formatter.ts # Utility for formatting JSON output. │ └── logo.ts # Utility for displaying the Ably logo ASCII art. diff --git a/docs/TODO.md b/docs/TODO.md deleted file mode 100644 index 086d9c5b..00000000 --- a/docs/TODO.md +++ /dev/null @@ -1,251 +0,0 @@ -# Tasks - -## Features - -- [ ] Consider changing the transport to use Ably instead of direct WebSocket to the terminal server -`timeout` is a good safe guard anyway to avoid human intervention when commands lock up. -- [x] Support new endpoint client options when available in our public APIs -> https://github.com/ably/ably-js/pull/1973 - -## API and Architecture - -- [ ] MCP server is not fully implemented, see log below. We should implement it so that it works fully for resources as expected. - ```text - 2025-04-11T23:03:05.759Z [ably] [info] Message from client: {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":24} - 2025-04-11T23:03:05.760Z [ably] [info] Message from server: {"jsonrpc":"2.0","id":24,"error":{"code":-32601,"message":"Method not found"}} - 2025-04-11T23:03:09.718Z [ably] [info] Message from client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":25} - 2025-04-11T23:03:10.969Z [ably] [info] Message from server: {"jsonrpc":"2.0","id":25,"result":{"resources":[{"name":"Default","uri":"ably://apps/cPr1qg","current":true},{"name":"Collaboration Tampermonkey","uri":"ably://apps/hdBgGA","current":false}]}} - 2025-04-11T23:03:10.969Z [ably] [info] Message from client: {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":26} - 2025-04-11T23:03:10.970Z [ably] [info] Message from server: {"jsonrpc":"2.0","id":26,"error":{"code":-32601,"message":"Method not found"}} - 2025-04-11T23:03:14.716Z [ably] [info] Message from client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":27} - 2025-04-11T23:03:15.346Z [ably] [info] Message from client: {"method":"resources/read","params":{"uri":"ably://apps/cPr1qg"},"jsonrpc":"2.0","id":28} - 2025-04-11T23:03:15.350Z [ably] [info] Message from server: {"jsonrpc":"2.0","id":28,"error":{"code":-32602,"message":"MCP error -32602: Resource ably://apps/cPr1qg not found"}} - 2025-04-11T23:03:15.493Z [ably] [info] Message from server: {"jsonrpc":"2.0","id":27,"result":{"resources":[{"name":"Default","uri":"ably://apps/cPr1qg","current":true},{"name":"Collaboration Tampermonkey","uri":"ably://apps/hdBgGA","current":false}]}} - ``` - -## Best Practices - -- [ ] Look for areas of unnecessary duplication as help.ts checking "commandId.includes('accounts login')" when the list of unsupported web CLI commands exists already in BaseCommand WEB_CLI_RESTRICTED_COMMANDS -- [ ] Now that we have .editorconfig, ensure all files adhere in one commit -- [ ] We are using a PNPM workspace, but I am not convinced that's a good thing. We should consider not letting the examples or React component dependencies affect the core CLI packaging. -- [ ] Implement token bearer auth for web CLI usage to minimise exposure of API keys and access tokens. This same thinking should apply to anonymous users (in spite of the API key being recycled), and the CLI needs to handle expiring tokens. See https://ably.atlassian.net/wiki/spaces/product/pages/4033511425/PDR-070+Web+CLI+technical+architecture?focusedCommentId=4051042310. - -## Bugs - -- [ ] The text inside the web terminal is now not wrapping, but instead it's scrolling off to the left showing a "<" char to the left of teh line. THis is not what is expected and should wrap to the next line. Need to tweak the bash settings. - -## Test coverage - -### Unit tests - -- [ ] **Core CLI & Infrastructure:** - - [x] `BaseCommand`: Flag parsing, error handling, output modes (JSON, pretty-JSON, Web CLI), API client creation (mocked), `showAuthInfoIfNeeded`, `setupCleanupHandler`, `parseApiKey`, `ensureAppAndKey` flows. - - [x] Test global flags (--host, --env, --control-host, --access-token, --api-key, --client-id, --verbose) propagation and overrides across commands. - - [x] Test invalid API key/token error flows and correct JSON error output - - [ ] Test interpolation and variable substitution in message templates (`{{.Count}}`, `{{.Timestamp}}`) - - [x] Test conflict error when using `--json` and `--pretty-json` together. - - [x] Test `parseApiKey` with invalid formats (missing key secret, malformed string). - - [ ] Test `setClientId` behavior for explicit `--client-id none` and default random UUID. - - [x] Test `ensureAppAndKey` fallback with env vars vs interactive prompts when config missing. - - [ ] Test error output JSON structure for invalid API key or token. - - [ ] `login.ts`: Mocked account login flow interaction. - - [x] `config.ts`: Mocked config read/write operations. - - [ ] `help` commands: Output generation, argument handling. - - [ ] Test `help ask` AI agent integration with mocked responses - - [ ] Test help command with and without web-cli-help flag - - [ ] `hooks/init/alias-command.ts`: Command aliasing logic. - - [x] `hooks/command_not_found/did-you-mean.ts`: Command suggestion logic. - - [x] Test Levenshtein distance calculation for command suggestions - - [x] Test formatting of suggestions - - [x] `services/config-manager.ts`: Test storage and retrieval of account, app, and API key information - - [x] `services/control-api.ts`: Test Control API request formatting and error handling - - [x] `services/interactive-helper.ts`: Test interactive prompts (mocked) - - [ ] Output Formatting Utilities: Table formatting, colorization logic. -- [ ] **Accounts:** - - [ ] `accounts login/logout/list/switch/current/stats`: Mock Control API calls, flag handling, output formats, config interactions. - - [ ] Test account login flow with browser opener mock - - [ ] Test account storing with and without aliases - - [ ] Test switch functionality between different accounts - - [ ] Test invalid access token input error and user guidance output -- [ ] **Apps:** - - [ ] `apps create/list/update/delete/switch/current/set-apns-p12`: Mock Control API calls, flag handling, output formats, config interactions. - - [ ] Test app creation with all available options - - [ ] Test app update with partial data - - [ ] Test P12 certificate file upload handling - - [ ] Test failure scenarios: duplicate app names, API error mapping - - [ ] `apps stats`: Mock Control API calls, flag handling, output formats. - - [ ] Test stats parsing and formatting for different time ranges - - [ ] Test --live polling functionality with mocked responses - - [ ] `apps logs history/subscribe`: Mock Control API/SDK, flag handling, output formats, SIGINT handling. - - [ ] Test logs filtering by types and parameters - - [ ] Test SIGINT handling for log subscription - - [ ] `apps channel-rules create/list/update/delete`: Mock Control API calls, flag handling, output formats. - - [ ] Test rule creation with various permission combinations - - [ ] Test namespace pattern validations -- [ ] **Auth:** - - [ ] `auth issue-ably-token/issue-jwt-token/revoke-token`: Mock SDK/API calls, flag handling, output formats. - - [ ] Test token generation with different capabilities - - [ ] Test token TTL parameter handling - - [ ] Test JWT token claims and signing - - [ ] Test invalid capability JSON and error reporting - - [ ] `auth keys create/list/get/update/revoke/switch/current`: Mock Control API calls, flag handling, output formats, config interactions. - - [ ] Test key creation with different capability sets - - [ ] Test key revocation flow including confirmation -- [ ] **Channels (Pub/Sub):** - - [x] `channels list/publish/subscribe/history/batch-publish`: Mock SDK/API calls, flag handling, encoding, output formats, SIGINT handling. - - [x] Test message encoding/decoding (including binary data) - - [x] Test channel reuse for multiple publish operations - - [x] Test batch publish with file input - - [x] Test `--count` and `--delay` options apply correct number/timing of messages - - [x] Test encryption flag (`--cipher`) produces encrypted messages and proper decryption - - [x] Test publish / subscribe / batch-publish plus --delay unit testing - - [x] `channels presence enter/subscribe`: Mock SDK, flag handling, output formats, SIGINT handling. - - [x] Test presence data handling (clientId, data payloads) - - [x] Test presence filtering by clientId - - [x] `channels occupancy get/subscribe`: Mock SDK, flag handling, output formats, SIGINT handling. - - [x] Test occupancy metrics parsing and formatting - - [x] Test live updates with simulated changes -- [ ] **Channel Rules (Legacy):** - - [ ] `channel-rule create/list/update/delete`: Mock Control API calls, flag handling, output formats. (Verify necessity). -- [ ] **Connections:** - - [ ] `connections stats`: Mock REST API call, flag handling, output formats. - - [ ] Test different stat aggregation periods - - [ ] Test connection types filtering - - [ ] `connections test`: Mock SDK connection attempts, flag handling, output formats. - - [ ] Test different transport options (WebSockets, HTTP) - - [ ] Test environment selection - - [ ] `connections logs`: Verify proxying to `logs connection subscribe`. -- [ ] **Logs:** - - [ ] `logs connection/connection-lifecycle/channel-lifecycle/push/app`: Mock SDK/API calls, flag handling, output formats, SIGINT handling. - - [ ] Test log filtering by types and channels - - [ ] Test rewind capability for supported channels - - [ ] Test formatted output for different log types - - [ ] Test rewind and live subscription flags interop and error conditions -- [ ] **Queues:** - - [ ] `queues create/list/delete`: Mock Control API calls, flag handling, output formats. - - [ ] Test queue creation with various TTL and size options - - [ ] Test deletion confirmation flow - - [ ] Test invalid TTL or size parameters produce meaningful errors -- [ ] **Integrations:** - - [ ] `integrations create/list/get/update/delete`: Mock Control API calls, flag handling, output formats. - - [ ] Test creation of different integration types - - [ ] Test source/target configuration validation - - [ ] Test invalid integration configuration fields rejected -- [ ] **Spaces:** - - [ ] `spaces list`: Mock SDK/API call, flag handling, output formats. - - [ ] `spaces members/locks/locations/cursors`: Mock Spaces SDK calls, flag handling, output formats, SIGINT handling for subscribe commands. - - [ ] Test location coordinate handling and updates - - [ ] Test cursor movement simulation - - [ ] Test lock acquisition and conflict handling - - [ ] Test auto-simulation of cursor movement when no coordinates provided -- [ ] **Rooms (Chat):** - - [ ] `rooms list`: Mock Chat SDK/API call, flag handling, output formats. - - [ ] `rooms messages/occupancy/reactions/presence/typing`: Mock Chat SDK calls, flag handling, output formats, SIGINT handling for subscribe commands. - - [ ] Test message formatting and rendering - - [ ] Test typing indicators state handling - - [ ] Test reactions to specific message ids - - [ ] Test `--count` and `--delay` interpolation identical to channels - - [ ] Test invalid room ID errors handled gracefully -- [ ] **Benchmarking:** - - [ ] `bench publisher/subscriber`: Mock SDK, complex flag interactions, parameter validation, summary generation logic. - - [ ] Test metrics calculation (throughput, latency) - - [ ] Test synchronization between publisher and subscriber - - [ ] Test throttling and rate limiting - - [ ] Test invalid rate limits (>20 msgs/sec) are rejected early -- [ ] **MCP:** - - [ ] `mcp start-server`: Server startup logic, argument parsing. - - [ ] Test MCP request handling for supported commands - - [ ] Test resource URI parsing - - [ ] Test timeout handling for long-running operations - - [ ] Test unsupported MCP methods return JSON-RPC "Method not found" - - [ ] Test resource listing and operations -- [ ] **Web CLI:** Test the terminal server with mocked Docker container. - - [x] Test WebSocket connection handling - - [x] Test command restriction enforcement - - [x] Test environment variable passing -- [ ] **Web CLI Restrictions:** For each restricted command, simulate `ABLY_WEB_CLI_MODE` and assert correct error message - -### Integration tests - -- [ ] **Core CLI:** `config set` -> `config get`, default/topic help output, command not found hook trigger. - - [x] Test that a user's config file is correctly written with expected values and structure - - [x] Test that topics show proper help information with examples - - [x] Test `ably help` without arguments lists all high-level topics correctly. - - [ ] Test interactive `ensureAppAndKey` prompts sequence in one CLI invocation. -- [ ] **Accounts:** Mocked login -> list -> current -> switch -> current -> logout sequence. - - [ ] Verify account state is properly maintained across commands - - [ ] Test that logout properly clears sensitive information -- [ ] **Apps:** Mocked create -> list -> current -> switch -> update -> delete sequence; mocked channel-rules CRUD sequence. - - [ ] Verify app selection state affects subsequent commands - - [ ] Test that app properties are properly persisted after update -- [ ] **Auth:** Mocked keys create -> list -> current -> switch -> update -> revoke sequence. - - [ ] Test that key capabilities are correctly applied - - [ ] Verify that revoked keys can no longer be used -- [x] **Channels (Pub/Sub):** Mocked publish -> subscribe, publish -> history, presence enter -> presence subscribe, occupancy get/subscribe sequences. - - [x] Test message delivery from publish to subscribe - - [x] Test that published messages appear in history - - [x] Test that presence state is correctly maintained - - [x] Test occupancy metrics correctly reflect channel activity -- [ ] **Queues:** Mocked create -> list -> delete sequence. - - [ ] Test queue configuration validation -- [ ] **Integrations:** Mocked create -> list -> get -> update -> delete sequence. - - [ ] Test that integration rules are properly applied -- [ ] **Spaces:** Mocked SDK interactions for members, locks, locations, cursors (e.g., enter -> subscribe, acquire -> get). - - [ ] Test concurrent lock operations - - [ ] Test member entry/exit notifications -- [ ] **Rooms (Chat):** Mocked SDK interactions for messages, occupancy, reactions, presence, typing (e.g., enter -> subscribe, send -> subscribe). - - [ ] Test message threading and ordering - - [ ] Test reaction aggregation -- [ ] **Benchmarking:** Local publisher/subscriber run (mocked SDK connections), report generation. - - [ ] Test report formatting and data accuracy -- [ ] **MCP:** Local server start, mock client connection, basic request/response test. - - [ ] Test resource listing and operations -- [ ] **Web CLI:** Test the terminal server with mocked Docker container. - - [x] Test WebSocket connection handling - - [x] Test command restriction enforcement - - [x] Test environment variable passing -- [ ] **Web CLI Restrictions:** For each restricted command, simulate `ABLY_WEB_CLI_MODE` and assert correct error message - -### End to End (e2e) tests - -- [ ] **Core CLI:** `ably --version`, `ably help`, `ably help ask`. - - [x] Verify version output matches package.json - - [ ] Test AI help agent with real queries - - [ ] Test interactive login flow end-to-end using pseudo-TTY simulation. -- [ ] **Accounts:** Real login flow (interactive/token), `list`, `current`, `stats`. - - [ ] Test end-to-end login with browser redirect - - [ ] Test stats retrieval with different time periods -- [ ] **Apps:** Real create, list, delete; real `stats`, `channel-rules list`, `apps logs subscribe`. - - [ ] Create app with specific settings and verify creation - - [ ] Test app lifecycle from creation to deletion -- [ ] **Auth:** Real keys create, list, revoke; real `issue-ably-token`. - - [ ] Create key with specific capabilities and verify they work - - [ ] Test token creation and use with client libraries -- [x] **Channels (Pub/Sub):** Real publish/subscribe, history, presence enter/subscribe, list. - - [x] Test cross-client communication - - [x] Test message persistence and retrieval - - [x] Test presence enter/leave operations - - [x] Test occupancy metrics for active channels - - [x] Test subscribe functionality with real-time message delivery -- [ ] **Connections:** Real `test`, `stats`. - - [ ] Test connection across different networks/environments - - [ ] Verify connection metrics are accurately reported -- [ ] **Logs:** Real `connection subscribe`, `push subscribe`. - - [ ] Test log delivery timing and completeness - - [ ] Verify push notification logs appear correctly -- [ ] **Queues:** Real create, list, delete. - - [ ] Test queue throughput and message retention -- [ ] **Integrations:** Real create, list, delete. - - [ ] Test integration with real external services (e.g., AWS, Google) -- [ ] **Spaces:** Real basic enter, subscribe members, set location, get-all locations. - - [ ] Test multi-client collaboration scenarios - - [ ] Test spatial data consistency across clients -- [ ] **Rooms (Chat):** Real enter presence, send message, subscribe messages. - - [ ] Test chat message delivery and ordering - - [ ] Test persistent history across sessions -- [ ] **Benchmarking:** Real publisher/subscriber run against Ably app. - - [ ] Test with various message sizes and rates - - [ ] Measure real-world performance metrics -- [ ] **Web Example:** Test the web terminal interface with real commands. - - [ ] Test terminal rendering and command execution - - [ ] Test session timeout and reconnection -- [ ] **Environment Overrides:** Test `--host`, `--env`, `--endpoint`, `--control-host` flags override endpoints diff --git a/docs/workplans/README.md b/docs/workplans/README.md index 4b2762eb..3986c28e 100644 --- a/docs/workplans/README.md +++ b/docs/workplans/README.md @@ -7,14 +7,14 @@ Each workplan file is a markdown file named with the date in the format [yyyy-mm ## Example brief for Agent ``` -Please look at all the tasks tagged with [feat/terminal-server-improvements] in @TODO.md, and write up a plan to implement all of the tagged features. +Please look at all the tasks tagged with [feat/terminal-server-improvements] and write up a plan to implement all of the tagged features. Note: 1) Your job is not to implement this yet, your job is to understand the code base fully, the features, and come up with a robust plan that allows another agent to follow the tasks and implement this all interatively and logically in groups of functionality. 2) Your plan must abide by all the best practices and requirements defined in the .cursor/rules and /docs folder. Make sure you have read every single file in @rules and @docs and any associated files that they reference that are relevant before you propose a plan. 3) Consider what test coverage is needed and include that in the plan. -4) Document your plan in the the docs/workplans folder with a file named and structured as described in @README.md, and ensure all documentation and rules, where applicable, are updated as you proceed through the plan, and all @TODO.md tasks are marked as complete when done. +4) Document your plan in the the docs/workplans folder with a file named and structured as described in @README.md, and ensure all documentation and rules, where applicable, are updated as you proceed through the plan. You need to use maximum effort researching this code base and the requested features so that we have a solid plan that can be exectued in steps and committed to git in each stage. ``` diff --git a/eslint.config.js b/eslint.config.js index 15044140..e1356967 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -216,13 +216,6 @@ export default [ "n/no-unpublished-import": "off", // Allow dev dependencies like chai in tests }, }, - // Configuration for MCP files with ModelContextProtocol SDK imports - { - files: ["src/mcp/**/*.ts"], - rules: { - "n/no-missing-import": "off" - } - }, // Prettier config must be last eslintConfigPrettier, { diff --git a/package.json b/package.json index 4857a86a..0bd30d29 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "@ably/chat": "^1.0.0", "@ably/spaces": "^0.4.0", "@inquirer/prompts": "^5.1.3", - "@modelcontextprotocol/sdk": "^1.8.0", "@oclif/core": "^4.2.10", "@oclif/plugin-autocomplete": "^3.2.30", "@oclif/plugin-warn-if-update-available": "^3.1.41", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1757323..3bc94f3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,9 +22,6 @@ importers: '@inquirer/prompts': specifier: ^5.1.3 version: 5.5.0 - '@modelcontextprotocol/sdk': - specifier: ^1.8.0 - version: 1.10.1 '@oclif/core': specifier: ^4.2.10 version: 4.2.10 @@ -1499,10 +1496,6 @@ packages: engines: {node: '>=18'} hasBin: true - '@modelcontextprotocol/sdk@1.10.1': - resolution: {integrity: sha512-xNYdFdkJqEfIaTVP1gPKoEvluACHZsHZegIoICX8DM1o6Qf3G5u2BQJHmgd0n4YgRPqqK/u1ujQvrgAxxSJT9w==} - engines: {node: '>=18'} - '@mswjs/interceptors@0.38.5': resolution: {integrity: sha512-YSa0sYrniWIfsJBabu/YRVG10v5bqWk0PprwERFDEd776nAe/aafkUd68g7vOhVK1xG2H+Pb8e3sAnCOu/V47w==} engines: {node: '>=18'} @@ -1661,111 +1654,133 @@ packages: resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.40.2': resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.40.0': resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.40.2': resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.40.0': resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.40.2': resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.40.0': resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.40.2': resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.40.0': resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loongarch64-gnu@4.40.2': resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.40.0': resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.40.2': resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.40.0': resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.40.2': resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.40.0': resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.40.2': resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.40.0': resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.40.2': resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.40.0': resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.40.2': resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.40.0': resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} @@ -2088,24 +2103,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.5': resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.5': resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.5': resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.5': resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==} @@ -2463,36 +2482,43 @@ packages: resolution: {integrity: sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.5.0': resolution: {integrity: sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.5.0': resolution: {integrity: sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.5.0': resolution: {integrity: sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-s390x-gnu@1.5.0': resolution: {integrity: sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.5.0': resolution: {integrity: sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.5.0': resolution: {integrity: sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.5.0': resolution: {integrity: sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg==} @@ -2644,10 +2670,6 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -2843,10 +2865,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} - bops@1.0.1: resolution: {integrity: sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==} @@ -2903,10 +2921,6 @@ packages: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3106,10 +3120,6 @@ packages: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} - content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3121,21 +3131,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - core-js-compat@3.41.0: resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - corser@2.0.1: resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} engines: {node: '>= 0.4.0'} @@ -3269,10 +3267,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -3329,9 +3323,6 @@ packages: engines: {node: '>=16'} hasBin: true - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -3349,10 +3340,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -3552,9 +3539,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3820,14 +3804,6 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventsource-parser@3.0.1: - resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.6: - resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} - engines: {node: '>=18.0.0'} - execa@1.0.0: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} @@ -3844,16 +3820,6 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.0: - resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} - engines: {node: '>= 16'} - peerDependencies: - express: ^4.11 || 5 || ^5.0.0-beta.1 - - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} - external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -3933,10 +3899,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -3988,17 +3950,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -4214,10 +4168,6 @@ packages: resolution: {integrity: sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==} engines: {node: '>=8.0.0'} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4308,10 +4258,6 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -4427,9 +4373,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4670,24 +4613,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -4816,14 +4763,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4855,10 +4794,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4963,10 +4898,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -5084,10 +5015,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - on-headers@1.0.2: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} @@ -5203,10 +5130,6 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -5255,10 +5178,6 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5288,10 +5207,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkce-challenge@5.0.0: - resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} - engines: {node: '>=16.20.0'} - playwright-core@1.52.0: resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} engines: {node: '>=18'} @@ -5374,10 +5289,6 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -5403,14 +5314,6 @@ packages: resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} engines: {node: '>= 0.6'} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -5564,10 +5467,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -5634,20 +5533,12 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} serve-handler@6.1.6: resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - serve@14.2.4: resolution: {integrity: sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==} engines: {node: '>= 14'} @@ -5665,9 +5556,6 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -5779,10 +5667,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -5966,10 +5850,6 @@ packages: to-utf8@0.0.1: resolution: {integrity: sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -6106,10 +5986,6 @@ packages: resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==} engines: {node: '>=16'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6191,10 +6067,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unrs-resolver@1.5.0: resolution: {integrity: sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA==} @@ -6471,11 +6343,6 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} - peerDependencies: - zod: ^3.24.1 - zod@3.24.3: resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} @@ -7803,21 +7670,6 @@ snapshots: - encoding - supports-color - '@modelcontextprotocol/sdk@1.10.1': - dependencies: - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.0(express@5.1.0) - pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.24.3 - zod-to-json-schema: 3.24.5(zod@3.24.3) - transitivePeerDependencies: - - supports-color - '@mswjs/interceptors@0.38.5': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -9195,7 +9047,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + vitest: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) '@vitest/utils@4.0.14': dependencies: @@ -9248,11 +9100,6 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - accepts@2.0.0: - dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9465,20 +9312,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.0: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.1 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - bops@1.0.1: dependencies: base64-js: 1.0.2 @@ -9543,8 +9376,6 @@ snapshots: bytes@3.0.0: {} - bytes@3.1.2: {} - cac@6.7.14: {} cacheable-lookup@5.0.4: {} @@ -9749,29 +9580,16 @@ snapshots: content-disposition@0.5.2: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 - content-type@1.0.5: {} convert-hrtime@3.0.0: {} convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - core-js-compat@3.41.0: dependencies: browserslist: 4.24.4 - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - corser@2.0.1: {} create-require@1.1.1: {} @@ -9885,8 +9703,6 @@ snapshots: delayed-stream@1.0.0: {} - depd@2.0.0: {} - dequal@2.0.3: {} detect-indent@7.0.1: {} @@ -9940,8 +9756,6 @@ snapshots: signal-exit: 4.0.2 time-span: 4.0.0 - ee-first@1.1.1: {} - ejs@3.1.10: dependencies: jake: 10.9.2 @@ -9954,8 +9768,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} - end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -10246,8 +10058,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -10649,12 +10459,6 @@ snapshots: eventemitter3@4.0.7: {} - eventsource-parser@3.0.1: {} - - eventsource@3.0.6: - dependencies: - eventsource-parser: 3.0.1 - execa@1.0.0: dependencies: cross-spawn: 6.0.6 @@ -10694,42 +10498,6 @@ snapshots: expect-type@1.2.2: {} - express-rate-limit@7.5.0(express@5.1.0): - dependencies: - express: 5.1.0 - - express@5.1.0: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.0(supports-color@8.1.1) - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.1 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -10801,17 +10569,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: - dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - find-up-simple@1.0.1: {} find-up@4.1.0: @@ -10860,12 +10617,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 - forwarded@0.2.0: {} - fraction.js@4.3.7: {} - fresh@2.0.0: {} - fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -11104,14 +10857,6 @@ snapshots: transitivePeerDependencies: - supports-color - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 @@ -11221,8 +10966,6 @@ snapshots: interpret@1.4.0: {} - ipaddr.js@1.9.1: {} - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -11325,8 +11068,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11692,10 +11433,6 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -11719,10 +11456,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - mime@1.6.0: {} mimic-fn@2.1.0: {} @@ -11791,8 +11524,6 @@ snapshots: negotiator@0.6.3: {} - negotiator@1.0.0: {} - nice-try@1.0.5: {} no-case@3.0.4: @@ -11944,10 +11675,6 @@ snapshots: - aws-crt - supports-color - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - on-headers@1.0.2: {} once@1.4.0: @@ -12080,8 +11807,6 @@ snapshots: dependencies: entities: 4.5.0 - parseurl@1.3.3: {} - pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -12122,8 +11847,6 @@ snapshots: path-to-regexp@6.3.0: {} - path-to-regexp@8.2.0: {} - path-type@4.0.0: {} pathe@2.0.3: {} @@ -12140,8 +11863,6 @@ snapshots: pirates@4.0.7: {} - pkce-challenge@5.0.0: {} - playwright-core@1.52.0: {} playwright@1.52.0: @@ -12209,11 +11930,6 @@ snapshots: proto-list@1.2.4: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -12233,15 +11949,6 @@ snapshots: range-parser@1.2.0: {} - range-parser@1.2.1: {} - - raw-body@3.0.0: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -12452,16 +12159,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.2 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.1 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.2.0 - transitivePeerDependencies: - - supports-color - rrweb-cssom@0.8.0: {} run-applescript@7.0.0: {} @@ -12519,22 +12216,6 @@ snapshots: semver@7.7.2: {} - send@1.2.0: - dependencies: - debug: 4.4.1 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - sentence-case@3.0.4: dependencies: no-case: 3.0.4 @@ -12551,15 +12232,6 @@ snapshots: path-to-regexp: 3.3.0 range-parser: 1.2.0 - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - serve@14.2.4: dependencies: '@zeit/schemas': 2.36.0 @@ -12598,8 +12270,6 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - setprototypeof@1.2.0: {} - shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -12719,8 +12389,6 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.1: {} - std-env@3.10.0: {} stdin-discarder@0.2.2: {} @@ -12920,8 +12588,6 @@ snapshots: to-utf8@0.0.1: {} - toidentifier@1.0.1: {} - totalist@3.0.1: {} tough-cookie@5.1.2: @@ -13059,12 +12725,6 @@ snapshots: type-fest@4.40.0: {} - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -13155,8 +12815,6 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: {} - unrs-resolver@1.5.0: optionalDependencies: '@unrs/resolver-binding-darwin-arm64': 1.5.0 @@ -13500,8 +13158,4 @@ snapshots: yoctocolors@2.1.1: {} - zod-to-json-schema@3.24.5(zod@3.24.3): - dependencies: - zod: 3.24.3 - zod@3.24.3: {} diff --git a/src/base-command.ts b/src/base-command.ts index 74ec2c65..398c325b 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -18,8 +18,6 @@ import { ChatClient } from "@ably/chat"; import isTestMode from "./utils/test-mode.js"; import isWebCliMode from "./utils/web-mode.js"; -// Export BaseFlags for potential use in other modules like MCP - // List of commands not allowed in web CLI mode - EXPORTED export const WEB_CLI_RESTRICTED_COMMANDS = [ // All account login/management commands are not valid in a web env where auth is handled by the website @@ -43,9 +41,6 @@ export const WEB_CLI_RESTRICTED_COMMANDS = [ // config only applicable to local env "config*", - - // MCP functionality is not available in the web CLI - "mcp*", ]; /* Additional restricted commands when running in anonymous web CLI mode */ @@ -76,7 +71,6 @@ export const INTERACTIVE_UNSUITABLE_COMMANDS = [ "autocomplete", // Autocomplete setup is not needed in interactive mode "config", // Config editing is not suitable for interactive mode "version", // Version is shown at startup and available via --version - "mcp", // MCP server functionality is not suitable for interactive mode ]; // List of commands that should not show account/app info @@ -325,8 +319,6 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { errorMessage = `Please log in at https://ably.com/login to use authentication features.`; } else if (commandId.startsWith("config")) { errorMessage = `Local configuration is not supported in the web CLI. Please install the CLI locally.`; - } else if (commandId.startsWith("mcp")) { - errorMessage = `MCP server functionality is not available in the web CLI. Please install the CLI locally.`; } else { errorMessage = `This command is not available in the web CLI. Please install the CLI locally.`; } @@ -355,8 +347,6 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { errorMessage = `You cannot switch API keys from within the web CLI. Please use the web interface to change keys.`; } else if (commandId.startsWith("config")) { errorMessage = `Local configuration is not supported in the web CLI version.`; - } else if (commandId.startsWith("mcp")) { - errorMessage = `MCP server functionality is not available in the web CLI. Please use the standalone CLI installation instead.`; } this.error(chalk.red(errorMessage)); diff --git a/src/commands/mcp/index.ts b/src/commands/mcp/index.ts deleted file mode 100644 index 52ee3f96..00000000 --- a/src/commands/mcp/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseTopicCommand } from "../../base-topic-command.js"; - -export default class McpCommands extends BaseTopicCommand { - protected topicName = "mcp"; - protected commandGroup = "Model Context Protocol (MCP)"; - - static description = - "Experimental Model Context Protocol (MCP) commands for AI tools to interact with Ably"; - - static examples = ["<%= config.bin %> <%= command.id %> start-server"]; -} diff --git a/src/commands/mcp/start-server.ts b/src/commands/mcp/start-server.ts deleted file mode 100644 index 2cfb018b..00000000 --- a/src/commands/mcp/start-server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AblyBaseCommand } from "../../base-command.js"; -import { AblyMcpServer } from "../../mcp/index.js"; -import { createConfigManager } from "../../services/config-manager.js"; - -export default class StartMcpServer extends AblyBaseCommand { - static description = - "Start an MCP server for AI tools to interact with Ably (currently experimental)"; - - static examples = ["<%= config.bin %> <%= command.id %>"]; - - static flags = { - // AblyBaseCommand already defines control-host as a global flag - }; - - async run(): Promise { - // Check if this command is allowed in web CLI mode - this.checkWebCliRestrictions(); - - // Parse flags - const { flags } = await this.parse(StartMcpServer); - - // Initialize Config Manager - const configManager = createConfigManager(); - - try { - // Start the server, write to stderr only - console.error("Starting Ably CLI MCP server..."); - - const server = new AblyMcpServer(configManager, { - controlHost: flags["control-host"], - }); - await server.start(); - - // The server.start() will block until the server is terminated - } catch (error) { - console.error( - "Failed to start MCP server:", - error instanceof Error ? error.message : String(error), - ); - this.error("Failed to start MCP server", { exit: 1 }); - } - } -} diff --git a/src/help.ts b/src/help.ts index ca359d65..5c977564 100644 --- a/src/help.ts +++ b/src/help.ts @@ -462,7 +462,7 @@ export default class CustomHelp extends Help { // Modify based on web CLI mode using the imported list if (this.webCliMode) { const isWebRestricted = WEB_CLI_RESTRICTED_COMMANDS.some((restricted) => { - // Handle wildcard patterns (e.g., "config*", "mcp*") + // Handle wildcard patterns (e.g., "config*") if (restricted.endsWith("*")) { const prefix = restricted.slice(0, -1); // Remove the asterisk return ( @@ -524,7 +524,7 @@ export default class CustomHelp extends Help { // In web mode, check if the command should be hidden using the imported list // Check if the commandId matches any restricted command pattern const isWebRestricted = WEB_CLI_RESTRICTED_COMMANDS.some((restricted) => { - // Handle wildcard patterns (e.g., "config*", "mcp*") + // Handle wildcard patterns (e.g., "config*") if (restricted.endsWith("*")) { const prefix = restricted.slice(0, -1); // Remove the asterisk return ( diff --git a/src/mcp/index.ts b/src/mcp/index.ts deleted file mode 100644 index 805d01a9..00000000 --- a/src/mcp/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AblyMcpServer } from "./mcp-server.js"; diff --git a/src/mcp/mcp-server.ts b/src/mcp/mcp-server.ts deleted file mode 100644 index 55a40edb..00000000 --- a/src/mcp/mcp-server.ts +++ /dev/null @@ -1,1291 +0,0 @@ -import { - McpServer, - ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; - -import { StdioServerTransport as StdioConnection } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; -import { URL } from "node:url"; - -import ChannelsHistory from "../commands/channels/history.js"; -import ChannelsList from "../commands/channels/list.js"; -import ChannelsPresenceSubscribe from "../commands/channels/presence/subscribe.js"; -import ChannelsPublish from "../commands/channels/publish.js"; -import ChannelsSubscribe from "../commands/channels/subscribe.js"; -import { ConfigManager } from "../services/config-manager.js"; -import Ably, { Connection } from "ably"; -import { getCliVersion } from "../utils/version.js"; -// Comment to explain why we're not using these directly but still need to import -// We need these types from control-api but using them through a different interface -import { - ControlApi as _ImportedControlApi, - App as ControlApp, - Key as ControlKey, -} from "../services/control-api.js"; - -// Maximum execution time for long-running operations (15 seconds) -const MAX_EXECUTION_TIME = 15_000; - -interface Message { - clientId?: string; - connectionId?: string; - data: unknown; - id: string; - name: string; - timestamp: number; - isRewind?: boolean; - [key: string]: unknown; -} - -interface PresenceMember { - action: number; - clientId?: string; - connectionId: string; - data: unknown; - id: string; - timestamp: number; - [key: string]: unknown; -} - -interface ChannelInfo { - name: string; - occupancy: Record; - status: Record; - [key: string]: unknown; -} - -// Define interfaces for Ably SDK responses - -// Aliasing types directly from Ably namespace -type RealtimePresenceMessage = Ably.PresenceMessage; -type RealtimeMessage = Ably.Message; -type RealtimeHistoryParams = Ably.RealtimeHistoryParams; -type PaginatedResult = Ably.PaginatedResult; - -// Simplified interfaces to avoid type compatibility issues -interface AblyChannel { - presence: { - get: () => Promise; - }; - history: ( - options?: RealtimeHistoryParams, - ) => Promise>; - subscribe: (callback: (message: RealtimeMessage) => void) => Promise; - unsubscribe: () => Promise; - publish: (name: string, data: unknown) => Promise; -} - -interface AblyClient { - channels: { - get: (channelName: string) => AblyChannel; - }; - request: ( - method: string, - path: string, - params?: Record, - ) => Promise>; - connection: Connection; -} - -interface ControlApiClient { - listApps: () => Promise; - getAppStats: (appId: string, options?: GetStatsParams) => Promise; - listKeys: (appId: string) => Promise; -} - -// Define interfaces for parameters - -// Define GetStatsParams based on Control API structure -interface GetStatsParams { - unit?: "minute" | "hour" | "day" | "month"; - direction?: "forwards" | "backwards"; - limit?: number; - start?: number; // epoch ms - end?: number; // epoch ms -} - -interface AppStatsParams { - app?: string; - end?: number; - limit?: number; - start?: number; - unit?: "minute" | "hour" | "day" | "month"; -} - -interface PublishParams { - channel: string; - message: unknown; - name?: string; - [key: string]: unknown; -} - -// Add types for command classes -type CommandClass = - | typeof ChannelsList - | typeof ChannelsHistory - | typeof ChannelsPublish - | typeof ChannelsSubscribe - | typeof ChannelsPresenceSubscribe; - -// Define the return type for executeCommand -type CommandResult = - | Message[] - | ChannelInfo[] - | PresenceMember[] - | { data: unknown; name: string }; - -// Define local types for resource parameters -type ParamsType = Record; -type ListParamsType = { variables?: ParamsType; signal?: AbortSignal }; - -export class AblyMcpServer { - private activeOperations: Set = new Set(); - private configManager: ConfigManager; - private controlHost?: string; - private server: McpServer; - - constructor( - configManager: ConfigManager, - options?: { controlHost?: string }, - ) { - this.configManager = configManager; - this.controlHost = options?.controlHost; - - // Initialize the MCP server - this.server = new McpServer({ - name: "Ably CLI", - version: process.env.npm_package_version || "1.0.0", - }); - } - - public async start(): Promise { - console.error("Initializing MCP server..."); - - // Set up client ID if not provided - this.setupClientId(); - - // Set up tools and resources - this.setupTools(); - this.setupResources(); - - // Create a stdio transport - const transport = new StdioConnection(); - - try { - // Connect the server to the transport - await this.server.connect(transport); - - console.error("MCP server ready, waiting for requests..."); - - // Register signal handlers for graceful shutdown - process.on("SIGINT", () => this.shutdown()); - process.on("SIGTERM", () => this.shutdown()); - } catch (error) { - console.error("Error starting MCP server:", error); - throw error; - } - } - - private async executeChannelsHistoryCommand( - args: string[], - ): Promise { - try { - // Parse arguments - const channelName = args.find((arg) => !arg.startsWith("-")) || ""; - if (!channelName || channelName === "--json") { - throw new Error("Channel name is required"); - } - - const limit = Number.parseInt(this.getArgValue(args, "--limit") || "100"); - const direction = this.getArgValue(args, "--direction") || "backwards"; - - // Get Ably client - const ably = await this.getAblyClient(); - - // Get channel - const channel = ably.channels.get(channelName); - - // Get history - const historyPage: PaginatedResult = - await channel.history({ - direction: direction as RealtimeHistoryParams["direction"], - limit, - }); - - return historyPage.items.map((msg: RealtimeMessage) => ({ - clientId: msg.clientId, - connectionId: msg.connectionId, - data: msg.data, - id: msg.id ?? `no-id-${Date.now()}`, - name: msg.name ?? "no-name", - timestamp: msg.timestamp ?? Date.now(), - })); - } catch (error: unknown) { - console.error("Error getting channel history:", error); - throw new Error( - `Failed to get channel history: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async executeChannelsListCommand( - args: string[], - ): Promise { - try { - // Parse arguments - const prefix = this.getArgValue(args, "--prefix"); - const limit = Number.parseInt(this.getArgValue(args, "--limit") || "100"); - - // Get Ably client - const ably = await this.getAblyClient(); - - // Build params - const params: Record = { limit }; - if (prefix) params.prefix = prefix; - - // Make the API request - const response = await ably.request("get", "/channels", params); - - if (response.statusCode !== 200) { - throw new Error(`Failed to list channels: ${response.statusCode}`); - } - - // Ensure response.items is an array before mapping - const items = Array.isArray(response.items) ? response.items : []; - - // Map response to simplified format - return items.map((channel: Record) => ({ - name: channel.channelId as string, - occupancy: channel.occupancy as Record, - status: (channel.status as Record) || {}, - })); - } catch (error: unknown) { - console.error("Error listing channels:", error); - throw new Error( - `Failed to list channels: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async executeChannelsPresenceCommand( - args: string[], - ): Promise { - try { - // Parse arguments - const channelName = - args.find((arg) => !arg.startsWith("-") && arg !== "--json") || ""; - if (!channelName) { - throw new Error("Channel name is required"); - } - - // Get Ably client - const ably = await this.getAblyClient(); - - // Get channel - const channel = ably.channels.get(channelName); - - // Get presence members - const presenceMembers = await channel.presence.get(); - - return presenceMembers.map((member: RealtimePresenceMessage) => ({ - action: - member.action === "present" || member.action === "enter" ? 1 : 0, - clientId: member.clientId, - connectionId: member.connectionId, - data: member.data, - id: member.id, - timestamp: member.timestamp, - })); - } catch (error: unknown) { - console.error("Error getting channel presence:", error); - throw new Error( - `Failed to get presence: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async executeChannelsPublishCommand( - args: string[] | PublishParams, - ): Promise<{ data: unknown; name: string }> { - try { - // Check if we're dealing with an array of arguments or an object - let channelName: string; - let message: unknown; - let name: string | undefined; - - if (Array.isArray(args)) { - // Parse arguments from command line - channelName = - args.find((arg) => !arg.startsWith("-") && arg !== "--json") || ""; - if (!channelName) { - throw new Error("Channel name is required"); - } - - // Get message argument (next non-flag after channel name) - const channelIndex = args.indexOf(channelName); - message = args[channelIndex + 1]; - if ( - !message || - (typeof message === "string" && message.startsWith("-")) - ) { - throw new Error("Message is required"); - } - - // Try to parse as JSON if possible - if (typeof message === "string") { - try { - message = JSON.parse(message); - } catch { - // Keep as string if not valid JSON - } - } - - name = this.getArgValue(args, "--name"); - } else if (typeof args === "object" && args !== null) { - // Handle direct object parameters (from MCP tool) - channelName = args.channel; - message = args.message; - name = args.name; - - if (!channelName) { - throw new Error("Channel name is required"); - } - - if (message === undefined) { - throw new Error("Message is required"); - } - } else { - throw new Error("Invalid arguments format"); - } - - // Get Ably client - const ably = await this.getAblyClient(); - - // Get channel and publish - const channel = ably.channels.get(channelName); - - if (name) { - await channel.publish(name, message); - return { data: message, name }; - } - - // If message is already an object with name/data, use that - if ( - typeof message === "object" && - message !== null && - "name" in message && - "data" in message - ) { - const msgName = String(message.name); - await channel.publish(msgName, message.data); - return { data: message.data, name: msgName }; - } - - // Default event name - await channel.publish("message", message); - return { data: message, name: "message" }; - } catch (error: unknown) { - console.error("Error publishing to channel:", error); - throw new Error( - `Failed to publish message: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async executeChannelsSubscribeCommand( - args: string[], - signal?: AbortSignal, - ): Promise { - try { - // Parse arguments - const channelName = - args.find((arg) => !arg.startsWith("-") && arg !== "--json") || ""; - if (!channelName) { - throw new Error("Channel name is required"); - } - - const rewind = Number.parseInt(this.getArgValue(args, "--rewind") || "0"); - - // Get Ably client - const ably = await this.getAblyClient(); - - // Get channel - const channel = ably.channels.get(channelName); - - // Subscribe for messages - const messages: Message[] = []; - - // Create a promise that resolves when signal is aborted or timeout - const abortPromise = new Promise((resolve) => { - if (signal) { - signal.addEventListener("abort", () => resolve()); - } - - // Also set a timeout - setTimeout(() => resolve(), MAX_EXECUTION_TIME); - }); - - // Create a promise for subscription - const _subscribePromise = new Promise((_resolve) => { - // Handle rewind if specified - if (rewind > 0) { - void (async () => { - try { - let currentPage: PaginatedResult | null = - await channel.history({ - direction: "backwards", - limit: rewind, - }); - while (currentPage) { - currentPage.items.forEach((msg: RealtimeMessage) => { - messages.push({ - clientId: msg.clientId, - connectionId: msg.connectionId, - data: msg.data, - id: msg.id ?? `no-id-${Date.now()}`, - name: msg.name ?? "no-name", - timestamp: msg.timestamp ?? Date.now(), - }); - }); - - if (!currentPage.hasNext()) { - break; - } - - currentPage = await currentPage.next(); - } - } catch (historyError) { - console.error( - "Error fetching history during rewind:", - historyError, - ); - } - })(); - } - - // Subscribe to new messages - const _subscription = channel.subscribe((msg: RealtimeMessage) => { - messages.push({ - clientId: msg.clientId as string | undefined, - connectionId: msg.connectionId as string, - data: msg.data, - id: msg.id as string, - name: msg.name as string, - timestamp: msg.timestamp as number, - }); - }); - }); - - // Wait for abort or timeout - await abortPromise; - - // Unsubscribe - await channel.unsubscribe(); - - return messages; - } catch (error: unknown) { - console.error("Error subscribing to channel:", error); - throw new Error( - `Failed to subscribe: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async executeCommand( - CommandClass: CommandClass, - args: string[] | PublishParams, - signal?: AbortSignal, - ): Promise { - try { - // Create direct execution functions for each command type - if (CommandClass === ChannelsList) { - return this.executeChannelsListCommand( - Array.isArray(args) ? args : ["--json", args.channel], - ); - } - - if (CommandClass === ChannelsHistory) { - return this.executeChannelsHistoryCommand( - Array.isArray(args) ? args : ["--json", args.channel], - ); - } - - if (CommandClass === ChannelsPublish) { - return this.executeChannelsPublishCommand(args); - } - - if (CommandClass === ChannelsSubscribe) { - return this.executeChannelsSubscribeCommand( - Array.isArray(args) ? args : ["--json", args.channel], - signal, - ); - } - - if (CommandClass === ChannelsPresenceSubscribe) { - return this.executeChannelsPresenceCommand( - Array.isArray(args) ? args : ["--json", args.channel], - ); - } - - throw new Error(`Unsupported command class: ${CommandClass.name}`); - } catch (error) { - console.error("Error executing command:", error); - throw error; - } - } - - private async getAblyClient(): Promise { - try { - // Assign the imported module to a variable first - const AblyModule = await import("ably"); - const Ably = AblyModule.default; - - // Get API key from config - const apiKey = this.configManager.getApiKey() || process.env.ABLY_API_KEY; - - if (!apiKey) { - throw new Error( - 'No API key configured. Please run "ably login" or set ABLY_API_KEY environment variable', - ); - } - - const clientOptions: Ably.ClientOptions & { - agents: Record; - } = { - clientId: process.env.ABLY_CLIENT_ID, - key: apiKey, - agents: { "ably-cli": getCliVersion() }, - }; - - // Create Ably REST client (not Realtime, to avoid connections) - // Note: We can't use createAblyRestClient here since this class doesn't extend AblyBaseCommand - const client = new Ably.Rest(clientOptions); - - // Type assertion to ensure compatibility with our interface - return client as unknown as AblyClient; - } catch (error) { - console.error("Error creating Ably client:", error); - throw new Error( - `Failed to create Ably client: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private getArgValue(args: string[], flag: string): string | undefined { - const index = args.indexOf(flag); - if (index !== -1 && index < args.length - 1) { - return args[index + 1]; - } - - return undefined; - } - - // Helper method to get a Control API instance - private async getControlApi(): Promise { - try { - const { ControlApi } = await import("../services/control-api.js"); - const accessToken = - process.env.ABLY_ACCESS_TOKEN || this.configManager.getAccessToken(); - - if (!accessToken) { - throw new Error( - 'No access token configured. Please run "ably login" to authenticate.', - ); - } - - return new ControlApi({ - accessToken, - controlHost: this.controlHost || process.env.ABLY_CONTROL_HOST, - }); - } catch (error: unknown) { - console.error("Error creating Control API client:", error); - throw new Error( - `Failed to create Control API client: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private setupClientId(): void { - // If client ID not provided, generate one with mcp prefix - if (!process.env.ABLY_CLIENT_ID) { - process.env.ABLY_CLIENT_ID = `mcp-${Math.random().toString(36).slice(2, 10)}`; - console.error(`Generated client ID: ${process.env.ABLY_CLIENT_ID}`); - } - } - - private setupResources(): void { - const resourceMethod = this.server.resource; - - // Channels resource - resourceMethod( - "channels", - new ResourceTemplate("ably://channels/{prefix?}", { - list: async (extra: ListParamsType) => { - try { - const args = ["--json"]; - const params = extra?.variables || {}; - const prefixParam = params.prefix; - const prefix = - typeof prefixParam === "string" - ? prefixParam - : Array.isArray(prefixParam) - ? prefixParam[0] - : undefined; - if (prefix) args.push("--prefix", prefix); - - const commandResult = await this.executeCommand(ChannelsList, args); - const channels: ChannelInfo[] = Array.isArray(commandResult) - ? (commandResult as ChannelInfo[]) - : []; - - return { - resources: channels.map((channel: ChannelInfo) => ({ - name: String(channel.name), - uri: `ably://channels/${channel.name}`, - })), - }; - } catch (error) { - console.error("Error listing channels:", error); - throw new Error( - `Failed to list channels: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - }), - async (uri: URL, params: ParamsType) => { - try { - const args = ["--json"]; - const prefixParam = params.prefix; - const prefix = - typeof prefixParam === "string" - ? prefixParam - : Array.isArray(prefixParam) - ? prefixParam[0] - : undefined; - if (prefix) args.push("--prefix", prefix); - - const commandResult = await this.executeCommand(ChannelsList, args); - const channels: ChannelInfo[] = Array.isArray(commandResult) - ? (commandResult as ChannelInfo[]) - : []; - - return { - contents: channels.map((channel: ChannelInfo) => ({ - text: JSON.stringify(channel, null, 2), - title: channel.name, - uri: `ably://channels/${channel.name}`, - })), - }; - } catch (error) { - console.error("Error fetching channels resource:", error); - throw new Error( - `Failed to fetch channels: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Channel History Resource - resourceMethod( - "channel_history", - new ResourceTemplate("ably://channel_history/{channel}", { - list: undefined, - }), - async (uri: URL, params: ParamsType) => { - try { - const args = ["--json"]; - const channelParam = params.channel; - const channel = - typeof channelParam === "string" - ? channelParam - : Array.isArray(channelParam) - ? channelParam[0] - : undefined; - if (channel) args.push(channel); - - const history = await this.executeCommand(ChannelsHistory, args); - - return { - contents: [ - { - text: JSON.stringify(history, null, 2), - title: `Message history for ${params.channel}`, - uri: uri.href, - }, - ], - }; - } catch (error) { - console.error("Error fetching channel history resource:", error); - throw new Error( - `Failed to fetch channel history: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Channel Presence Resource - resourceMethod( - "channel_presence", - new ResourceTemplate("ably://channel_presence/{channel}", { - list: undefined, - }), - async (uri: URL, params: ParamsType) => { - try { - const args = ["--json"]; - const channelParam = params.channel; - const channel = - typeof channelParam === "string" - ? channelParam - : Array.isArray(channelParam) - ? channelParam[0] - : undefined; - if (channel) args.push(channel); - - const presence = await this.executeCommand( - ChannelsPresenceSubscribe, - args, - ); - - return { - contents: [ - { - text: JSON.stringify(presence, null, 2), - title: `Presence members for ${params.channel}`, - uri: uri.href, - }, - ], - }; - } catch (error) { - console.error("Error fetching channel presence resource:", error); - throw new Error( - `Failed to fetch channel presence: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Apps Resource - resourceMethod( - "apps", - new ResourceTemplate("ably://apps", { - list: async () => { - try { - const controlApi = await this.getControlApi(); - const apps = await controlApi.listApps(); - - // Add the current app indicator - const currentAppId = this.configManager.getCurrentAppId(); - - return { - resources: apps.map((app: ControlApp) => ({ - current: app.id === currentAppId, - name: app.name, - uri: `ably://apps/${app.id}`, - })), - }; - } catch (error) { - console.error("Error listing apps:", error); - throw new Error( - `Failed to list apps: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - }), - async (uri: URL) => { - try { - const controlApi = await this.getControlApi(); - const apps = await controlApi.listApps(); - - // Add the current app indicator - const currentAppId = this.configManager.getCurrentAppId(); - const appsWithCurrent = apps.map((app: ControlApp) => ({ - ...app, - current: app.id === currentAppId, - })); - - return { - contents: [ - { - text: JSON.stringify(appsWithCurrent, null, 2), - title: "Ably Apps", - uri: uri.href, - }, - ], - }; - } catch (error) { - console.error("Error fetching apps resource:", error); - throw new Error( - `Failed to fetch apps: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // App Stats Resource - resourceMethod( - "app_stats", - new ResourceTemplate("ably://apps/{appId}/stats", { list: undefined }), - async (uri: URL, params: ParamsType) => { - try { - // Use the app ID from the URI or fall back to default - const appIdParam = params.appId; - const appId = - (typeof appIdParam === "string" - ? appIdParam - : Array.isArray(appIdParam) - ? appIdParam[0] - : undefined) || this.configManager.getCurrentAppId(); - - if (!appId) { - throw new Error("No app ID provided and no default app selected"); - } - - const controlApi = await this.getControlApi(); - - // Get stats for the last 24 hours - const now = new Date(); - const start = now.getTime() - 24 * 60 * 60 * 1000; // 24 hours ago - const end = now.getTime(); - - const stats = await controlApi.getAppStats(appId as string, { - end, - limit: 10, - start, - unit: "minute", - }); - - return { - contents: [ - { - text: JSON.stringify(stats, null, 2), - title: `Statistics for app ${appId}`, - uri: uri.href, - }, - ], - }; - } catch (error) { - console.error("Error fetching app stats resource:", error); - throw new Error( - `Failed to fetch app stats: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // App Keys Resource - resourceMethod( - "app_keys", - new ResourceTemplate("ably://apps/{appId}/keys", { list: undefined }), - async (uri: URL, params: ParamsType) => { - try { - // Use the app ID from the URI or fall back to default - const appIdParam = params.appId; - const appId = - (typeof appIdParam === "string" - ? appIdParam - : Array.isArray(appIdParam) - ? appIdParam[0] - : undefined) || this.configManager.getCurrentAppId(); - - if (!appId) { - throw new Error("No app ID provided and no default app selected"); - } - - const controlApi = await this.getControlApi(); - const keys = await controlApi.listKeys(appId as string); - - // Add the current key indicator - const currentKeyId = this.configManager.getKeyId(appId as string); - const currentKeyName = - currentKeyId && currentKeyId.includes(".") - ? currentKeyId - : currentKeyId - ? `${appId}.${currentKeyId}` - : undefined; - - const keysWithCurrent = keys.map((key: ControlKey) => { - const keyName = `${key.appId}.${key.id}`; - return { - ...key, - current: keyName === currentKeyName, - keyName, - capabilities: {}, // Add missing capabilities property - }; - }); - - return { - contents: [ - { - text: JSON.stringify(keysWithCurrent, null, 2), - title: `API Keys for app ${appId}`, - uri: uri.href, - }, - ], - }; - } catch (error) { - console.error("Error fetching app keys resource:", error); - throw new Error( - `Failed to fetch app keys: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - } - - private setupTools(): void { - // List Channels tool - this.server.tool( - "list_channels", - "List active channels using the channel enumeration API", - { - limit: z - .number() - .optional() - .describe("Maximum number of channels to return"), - prefix: z.string().optional().describe("Filter channels by prefix"), - }, - async (_params: { limit?: number; prefix?: string }) => { - try { - const result = await this.executeCommand(ChannelsList, [ - "--json", - ...(_params.prefix ? ["--prefix", _params.prefix] : []), - ...(_params.limit ? ["--limit", _params.limit.toString()] : []), - ]); - - return { - content: [ - { - text: JSON.stringify(result, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error listing channels:", error); - throw new Error( - `Failed to list channels: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Channel History tool - this.server.tool( - "get_channel_history", - "Retrieve message history for a channel", - { - channel: z.string().describe("Name of the channel to get history for"), - direction: z - .enum(["forwards", "backwards"]) - .optional() - .describe("Direction of message history"), - limit: z - .number() - .optional() - .describe("Maximum number of messages to retrieve"), - }, - async (_params: { - channel: string; - direction?: string; - limit?: number; - }) => { - try { - const args = ["--json", _params.channel]; - if (_params.limit) args.push("--limit", _params.limit.toString()); - if (_params.direction) args.push("--direction", _params.direction); - - const result = await this.executeCommand(ChannelsHistory, args); - return { - content: [ - { - text: JSON.stringify(result, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error getting channel history:", error); - throw new Error( - `Failed to get channel history: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Publish to Channel tool - this.server.tool( - "publish_to_channel", - "Publish a message to an Ably channel", - { - channel: z.string().describe("Name of the channel to publish to"), - message: z - .string() - .describe("Message content to publish (can be string or JSON)"), - name: z - .string() - .optional() - .describe("Event name (optional, defaults to 'message')"), - }, - async (_params: { channel: string; message: string; name?: string }) => { - try { - // Try to parse message as JSON if it's a string - let messageContent = _params.message; - if (typeof messageContent === "string") { - try { - messageContent = JSON.parse(messageContent); - } catch { - // Keep as string if not valid JSON - } - } - - // Create parameters object with parsed message - const paramsWithParsedMessage = { - ..._params, - message: messageContent, - }; - - // Pass parameters in the format expected by executeChannelsPublishCommand - const result = await this.executeChannelsPublishCommand( - paramsWithParsedMessage, - ); - return { - content: [ - { - text: JSON.stringify(result, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error publishing to channel:", error); - throw new Error( - `Failed to publish to channel: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Channel Presence tool - this.server.tool( - "get_channel_presence", - "Get presence members for a channel", - { - channel: z.string().describe("Name of the channel to get presence for"), - }, - async (_params: { channel: string }) => { - try { - const args = ["--json", _params.channel]; - - const result = await this.executeCommand( - ChannelsPresenceSubscribe, - args, - ); - return { - content: [ - { - text: JSON.stringify(result, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error getting channel presence:", error); - throw new Error( - `Failed to get channel presence: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Apps List tool - this.server.tool( - "list_apps", - "List Ably apps within the current account", - { - format: z - .enum(["json", "pretty"]) - .optional() - .default("json") - .describe("Output format (json or pretty)"), - }, - async (_params: { format?: string }) => { - try { - // Create a Control API instance - const controlApi = await this.getControlApi(); - - // Get the apps - const apps = await controlApi.listApps(); - - // Add the current app indicator - const currentAppId = this.configManager.getCurrentAppId(); - const appsWithCurrent = apps.map((app: ControlApp) => ({ - ...app, - current: app.id === currentAppId, - })); - - return { - content: [ - { - text: JSON.stringify(appsWithCurrent, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error listing apps:", error); - throw new Error( - `Failed to list apps: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Apps Stats tool - this.server.tool( - "get_app_stats", - "Get statistics for an Ably app", - { - app: z - .string() - .optional() - .describe( - "App ID to get stats for (uses current app if not provided)", - ), - end: z - .number() - .optional() - .describe("End time in milliseconds since epoch"), - limit: z - .number() - .optional() - .default(10) - .describe("Maximum number of stats records to return"), - start: z - .number() - .optional() - .describe("Start time in milliseconds since epoch"), - unit: z - .enum(["minute", "hour", "day", "month"]) - .optional() - .default("minute") - .describe("Time unit for stats"), - }, - async (_params: AppStatsParams) => { - try { - // Use provided app ID or fall back to default app ID - const appId = _params.app || this.configManager.getCurrentAppId(); - - if (!appId) { - throw new Error("No app ID provided and no default app selected"); - } - - // Create a Control API instance - const controlApi = await this.getControlApi(); - - // If no start/end time provided, use the last 24 hours - const now = new Date(); - const start = _params.start || now.getTime() - 24 * 60 * 60 * 1000; // 24 hours ago - const end = _params.end || now.getTime(); - - // Get the stats - const stats = await controlApi.getAppStats(appId, { - end, - limit: _params.limit, - start, - unit: _params.unit, - }); - - return { - content: [ - { - text: JSON.stringify(stats, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error getting app stats:", error); - throw new Error( - `Failed to get app stats: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - - // Auth Keys List tool - this.server.tool( - "list_auth_keys", - "List API keys for an Ably app", - { - app: z - .string() - .optional() - .describe( - "App ID to list keys for (uses current app if not provided)", - ), - }, - async (_params: { app?: string }) => { - try { - // Get app ID from parameter or current config - const appId = _params.app || this.configManager.getCurrentAppId(); - - if (!appId) { - throw new Error("No app specified"); - } - - // Create a Control API instance - const controlApi = await this.getControlApi(); - - // Get the keys - const keys = await controlApi.listKeys(appId as string); - - // Add the current key indicator - const currentKeyId = this.configManager.getKeyId(appId as string); - const currentKeyName = - currentKeyId && currentKeyId.includes(".") - ? currentKeyId - : currentKeyId - ? `${appId}.${currentKeyId}` - : undefined; - - const keysWithCurrent = keys.map((key: ControlKey) => { - const keyName = `${key.appId}.${key.id}`; - return { - ...key, - current: keyName === currentKeyName, - keyName, // Add the full key name - capabilities: {}, // Add missing capabilities property - }; - }); - - return { - content: [ - { - text: JSON.stringify(keysWithCurrent, null, 2), - type: "text", - }, - ], - }; - } catch (error) { - console.error("Error listing keys:", error); - throw new Error( - `Failed to list keys: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - ); - } - - private shutdown(): void { - console.error("MCP server shutting down..."); - - // Abort any active operations - for (const controller of this.activeOperations) { - controller.abort(); - } - - // Exit process - - process.exit(0); - } -} diff --git a/src/types/modelcontextprotocol.d.ts b/src/types/modelcontextprotocol.d.ts deleted file mode 100644 index 1617de12..00000000 --- a/src/types/modelcontextprotocol.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -declare module "@modelcontextprotocol/sdk" { - export interface ServerOptions { - capabilities?: { - markdown?: boolean; - schemas?: boolean; - }; - resources?: Resource[]; - tools?: Tool[]; - } - - export interface Resource { - description: string; - fetch: (params?: any) => Promise; - name: string; - parameters?: any[]; - title: string; - } - - export interface Tool { - description: string; - execute: (params: any) => Promise; - name: string; - parameters: any; - } - - export interface Server { - start(): Promise; - } - - export function createServer(options: ServerOptions): Server; - export function createResource( - resource: { parameters?: any[] } & Omit, - ): Resource; - export function createStringParam( - name: string, - description: string, - options?: { required: boolean }, - ): any; -} diff --git a/test/integration/interactive-mode.test.ts b/test/integration/interactive-mode.test.ts index a5538201..789b1e95 100644 --- a/test/integration/interactive-mode.test.ts +++ b/test/integration/interactive-mode.test.ts @@ -87,7 +87,7 @@ describe("Interactive Mode Command Tests", function () { }); describe("Interactive Unsuitable Commands", function () { - const unsuitableCommands = ["autocomplete", "config", "mcp"]; + const unsuitableCommands = ["autocomplete", "config"]; for (const cmd of unsuitableCommands) { it(`should block ${cmd} command in interactive mode`, function (done) { diff --git a/test/unit/base/base-command-enhanced.test.ts b/test/unit/base/base-command-enhanced.test.ts index 4d233cdd..97f90841 100644 --- a/test/unit/base/base-command-enhanced.test.ts +++ b/test/unit/base/base-command-enhanced.test.ts @@ -159,10 +159,6 @@ describe("AblyBaseCommand - Enhanced Coverage", function () { expect(command.testIsAllowedInWebCliMode("accounts:logout")).toBe(false); }); - it("should restrict MCP commands", function () { - expect(command.testIsAllowedInWebCliMode("mcp:start-server")).toBe(false); - }); - it("should allow help commands", function () { expect(command.testIsAllowedInWebCliMode("help")).toBe(true); expect(command.testIsAllowedInWebCliMode("help:contact")).toBe(true); diff --git a/test/unit/base/base-command.test.ts b/test/unit/base/base-command.test.ts index c518ee19..880f2e31 100644 --- a/test/unit/base/base-command.test.ts +++ b/test/unit/base/base-command.test.ts @@ -303,16 +303,6 @@ describe("AblyBaseCommand", function () { expectedError: /Local configuration is not supported in the web CLI\. Please install the CLI locally/, }, - { - id: "mcp", - expectedError: - /MCP server functionality is not available in the web CLI\. Please install the CLI locally/, - }, - { - id: "mcp:start-server", - expectedError: - /MCP server functionality is not available in the web CLI\. Please install the CLI locally/, - }, { id: "accounts:current", expectedError: @@ -377,8 +367,6 @@ describe("AblyBaseCommand", function () { expect(command.testIsAllowedInWebCliMode("accounts:logout")).toBe(false); expect(command.testIsAllowedInWebCliMode("config")).toBe(false); expect(command.testIsAllowedInWebCliMode("config:set")).toBe(false); - expect(command.testIsAllowedInWebCliMode("mcp")).toBe(false); - expect(command.testIsAllowedInWebCliMode("mcp:start-server")).toBe(false); }); it("should return true for allowed commands", function () { diff --git a/test/unit/commands/interactive-autocomplete.test.ts b/test/unit/commands/interactive-autocomplete.test.ts index 81d6c272..28dc712b 100644 --- a/test/unit/commands/interactive-autocomplete.test.ts +++ b/test/unit/commands/interactive-autocomplete.test.ts @@ -269,9 +269,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { { id: "config:get", hidden: false }, { id: "config:set", hidden: false }, { id: "version", hidden: false }, - // MCP commands (restricted in web CLI) - { id: "mcp", hidden: false }, - { id: "mcp:start", hidden: false }, // Hidden command (should always be filtered) { id: "hidden-command", hidden: true }, ], @@ -303,7 +300,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { expect(commands).not.toContain("autocomplete"); expect(commands).not.toContain("config"); expect(commands).not.toContain("version"); - expect(commands).not.toContain("mcp"); // MCP is not suitable for interactive mode // Should include these commands expect(commands).toContain("apps"); @@ -339,7 +335,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { // Should NOT include web CLI restricted commands expect(commands).not.toContain("config"); // config* restricted - expect(commands).not.toContain("mcp"); // mcp* restricted // Should include commands that are only partially restricted expect(commands).toContain("accounts"); // only specific subcommands are restricted @@ -391,7 +386,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { expect(commands).not.toContain("queues"); // restricted in anonymous mode expect(commands).not.toContain("logs"); // restricted in anonymous mode expect(commands).not.toContain("config"); // restricted in web CLI mode - expect(commands).not.toContain("mcp"); // restricted in web CLI mode // Should still include some commands expect(commands).toContain("channels"); // channels root is allowed @@ -450,8 +444,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { expect(isRestricted("config")).toBe(true); // config* matches config expect(isRestricted("config:get")).toBe(true); // config* matches config:get expect(isRestricted("config:set")).toBe(true); // config* matches config:set - expect(isRestricted("mcp")).toBe(true); // mcp* matches mcp - expect(isRestricted("mcp:start")).toBe(true); // mcp* matches mcp:start // Test exact matches expect(isRestricted("accounts:login")).toBe(true); @@ -470,7 +462,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { const normalCommands = ( interactiveCommand as any ).getTopLevelCommands(); - expect(normalCommands).not.toContain("mcp"); // MCP is always unsuitable for interactive mode expect(normalCommands).toContain("accounts"); // accounts is available in normal mode // Clear cache @@ -481,7 +472,6 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { const webCliCommands = ( interactiveCommand as any ).getTopLevelCommands(); - expect(webCliCommands).not.toContain("mcp"); // Still filtered // accounts:login, logout, switch are restricted but accounts itself is visible expect(webCliCommands).toContain("accounts"); }); diff --git a/test/unit/commands/mcp/mcp.test.ts b/test/unit/commands/mcp/mcp.test.ts deleted file mode 100644 index 6b6f1a85..00000000 --- a/test/unit/commands/mcp/mcp.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { Config } from "@oclif/core"; - -import McpStartServer from "../../../../src/commands/mcp/start-server.js"; -import { AblyMcpServer } from "../../../../src/mcp/mcp-server.js"; - -// Testable subclass for MCP start server command -class TestableMcpStartServer extends McpStartServer { - private _parseResult: any; - public mockMcpServer: any; - public mockConfigManager: any; - public constructorArgs: any[] = []; - public startCalled = false; - - public setParseResult(result: any) { - this._parseResult = result; - } - - public override async parse() { - return this._parseResult; - } - - public override async run(): Promise { - // Parse flags like the real implementation - const { flags } = await this.parse(); - - // Simulate the constructor call - this.constructorArgs = [ - this.mockConfigManager, - { controlHost: flags["control-host"] }, - ]; - - // Simulate calling start - this.startCalled = true; - if (this.mockMcpServer?.start) { - await this.mockMcpServer.start(); - } - } - - protected override checkWebCliRestrictions() { - // Skip web CLI restrictions for testing - } - - protected override interactiveHelper = { - confirm: vi.fn().mockResolvedValue(true), - promptForText: vi.fn().mockResolvedValue("fake-input"), - promptToSelect: vi.fn().mockResolvedValue("fake-selection"), - } as any; -} - -describe("mcp commands", function () { - let mockConfig: Config; - - beforeEach(function () { - mockConfig = { runHook: vi.fn() } as unknown as Config; - }); - - describe("mcp start-server", function () { - let command: TestableMcpStartServer; - let startStub: ReturnType; - let mockMcpServer: any; - let mockConfigManager: any; - - beforeEach(function () { - command = new TestableMcpStartServer([], mockConfig); - - startStub = vi.fn().mockImplementation(async () => {}); - mockMcpServer = { - start: startStub, - }; - - mockConfigManager = { - getConfig: vi.fn().mockReturnValue({ - defaultAccount: { alias: "test-account" }, - accounts: { "test-account": { accessToken: "test-token" } }, - }), - saveConfig: vi.fn().mockImplementation(async () => {}), - }; - - command.mockMcpServer = mockMcpServer; - command.mockConfigManager = mockConfigManager; - - command.setParseResult({ - flags: {}, - args: {}, - argv: [], - raw: [], - }); - }); - - it("should start MCP server successfully", async function () { - await command.run(); - - expect(command.startCalled).toBe(true); - expect(startStub).toHaveBeenCalledOnce(); - }); - - it("should pass control host option to MCP server", async function () { - command.setParseResult({ - flags: { "control-host": "custom.ably.io" }, - args: {}, - argv: [], - raw: [], - }); - - await command.run(); - - // Check that the constructor would have been called with the correct options - expect(command.constructorArgs).toHaveLength(2); - expect(command.constructorArgs[1]).toEqual({ - controlHost: "custom.ably.io", - }); - }); - - it("should handle MCP server startup errors", async function () { - startStub.mockRejectedValue(new Error("Failed to bind to port")); - - await expect(command.run()).rejects.toThrow("Failed to bind to port"); - }); - }); - - describe("AblyMcpServer", function () { - let mockConfigManager: any; - let server: AblyMcpServer; - - beforeEach(function () { - mockConfigManager = { - getConfig: vi.fn().mockReturnValue({ - defaultAccount: { alias: "test-account" }, - accounts: { "test-account": { accessToken: "test-token" } }, - }), - saveConfig: vi.fn().mockImplementation(async () => {}), - }; - }); - - afterEach(function () { - // Clean up server if it was created - server = null as any; - vi.restoreAllMocks(); - }); - - it("should initialize with default options", function () { - server = new AblyMcpServer(mockConfigManager); - - expect(server).toBeInstanceOf(AblyMcpServer); - }); - - it("should initialize with custom control host", function () { - const options = { controlHost: "custom.ably.io" }; - server = new AblyMcpServer(mockConfigManager, options); - - expect(server).toBeInstanceOf(AblyMcpServer); - }); - - it("should handle missing configuration gracefully", function () { - mockConfigManager.getConfig.mockReturnValue({}); - - expect(() => { - server = new AblyMcpServer(mockConfigManager); - }).not.toThrow(); - }); - - describe("MCP protocol operations", function () { - beforeEach(function () { - server = new AblyMcpServer(mockConfigManager); - }); - - it("should expose available start method", function () { - // Since AblyMcpServer is a complex class, we'll test the basic structure - // In a real implementation, you'd test the MCP protocol methods - expect(server).toHaveProperty("start"); - expect(typeof server.start).toBe("function"); - }); - - // eslint-disable-next-line vitest/no-disabled-tests - it.skip("should handle basic server lifecycle", async function () { - // See: https://github.com/ably/cli/issues/70 - // This test requires a different approach to test server lifecycle - // without emitting process signals in unit tests - const exitSpy = vi.spyOn(process, "exit"); - - // Start the server in the background - const _startPromise = server.start(); - - // Give it a moment to start - await new Promise((resolve) => setTimeout(resolve, 10)); - - // TODO: Implement proper server shutdown mechanism for testing - // that doesn't rely on process.emit("SIGINT") - - // Verify that process.exit was called - expect(exitSpy).toHaveBeenCalledWith(0); - }); - }); - - describe("error handling", function () { - it("should handle server startup with invalid configuration", function () { - // Test with null configuration - mockConfigManager.getConfig.mockReturnValue(null); - - server = new AblyMcpServer(mockConfigManager); - - // Server should still be created, errors would occur on start() - expect(server).toBeInstanceOf(AblyMcpServer); - }); - - it("should handle empty configuration", function () { - mockConfigManager.getConfig.mockReturnValue({}); - - server = new AblyMcpServer(mockConfigManager); - - expect(server).toBeInstanceOf(AblyMcpServer); - }); - - it("should handle missing config manager methods", function () { - const incompleteConfigManager = { - getConfig: vi.fn().mockReturnValue({}), - // Missing other methods - }; - - server = new AblyMcpServer(incompleteConfigManager as any); - - expect(server).toBeInstanceOf(AblyMcpServer); - }); - }); - }); -}); diff --git a/test/unit/help/interactive-help.test.ts b/test/unit/help/interactive-help.test.ts index 4fb9b194..dd4a2bf3 100644 --- a/test/unit/help/interactive-help.test.ts +++ b/test/unit/help/interactive-help.test.ts @@ -181,7 +181,6 @@ ably> channels publish test "msg2" expect(help.shouldDisplay({ id: "accounts:list" } as any)).toBe(false); expect(help.shouldDisplay({ id: "apps:create" } as any)).toBe(false); expect(help.shouldDisplay({ id: "config" } as any)).toBe(false); - expect(help.shouldDisplay({ id: "mcp:start" } as any)).toBe(false); // These should still be visible expect(help.shouldDisplay({ id: "channels:publish" } as any)).toBe(true); diff --git a/test/unit/help/web-cli-help.test.ts b/test/unit/help/web-cli-help.test.ts index 95b091d2..7966dc08 100644 --- a/test/unit/help/web-cli-help.test.ts +++ b/test/unit/help/web-cli-help.test.ts @@ -208,7 +208,6 @@ describe("CLI Help", function () { hidden: false, }, { name: "config", description: "Config command", hidden: false }, - { name: "mcp", description: "MCP command", hidden: false }, ]; const mockConfig = createMockConfig(mockCommands, mockTopics); @@ -228,9 +227,8 @@ describe("CLI Help", function () { // Should show allowed command expect(output).toContain("channels"); - // Should NOT show commands matching wildcard patterns (config*, mcp*) + // Should NOT show commands matching wildcard patterns (config*) expect(output).not.toContain("config"); - expect(output).not.toContain("mcp"); }); it("should show common commands in anonymous mode", async function () { @@ -335,7 +333,6 @@ describe("CLI Help", function () { // Test restricted commands expect(help.shouldDisplay({ id: "accounts:login" } as any)).toBe(false); expect(help.shouldDisplay({ id: "config" } as any)).toBe(false); - expect(help.shouldDisplay({ id: "mcp:start" } as any)).toBe(false); // Test allowed commands expect(help.shouldDisplay({ id: "channels:publish" } as any)).toBe(