From e328e9583e065b620de3a65230c4b7d4402218d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 07:51:36 +0000 Subject: [PATCH 1/3] Fix stale MCP tool names in the formo-analytics skill - `wallet_profiles` and `project_retention` are no longer in the MCP allow-list; reference `retention` and `search_profile` instead. - Note the MCP server's management tool coverage (alerts, boards/charts, contracts, segments, profile writes, wallet import) so agents don't assume the CLI is the only management surface. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BWrVEx5C374qkrAQUCgx7y --- skills/formo-analytics/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skills/formo-analytics/SKILL.md b/skills/formo-analytics/SKILL.md index d463114..30ecfd9 100644 --- a/skills/formo-analytics/SKILL.md +++ b/skills/formo-analytics/SKILL.md @@ -50,7 +50,7 @@ Useful MCP tools include: - `list_endpoints`, `list_datasources` - `text_to_sql`, `execute_query`, `explore_data` -- published tools such as `kpis`, `lifecycle`, `top_events`, `top_pages`, `top_sources`, `top_locations`, `revenue_overview`, `wallet_profiles`, and `project_retention` +- published tools such as `kpis`, `lifecycle`, `top_events`, `top_pages`, `top_sources`, `top_locations`, `revenue_overview`, `retention`, and `search_profile` - `search_formo_docs` and `query_docs_filesystem_formo_docs` for grounded product and API questions The live endpoint list varies by project. Prefer discovery over assuming a tool exists. @@ -87,6 +87,8 @@ Use `--timestamp ` with `profiles get` to return the stored wallet-enr ## Manage Formo resources +The MCP server exposes management tools for the same resources — alerts (including `test_alert`), boards/charts (including `reorder_charts` and `execute_saved_chart`), contracts (including `list_contract_recommendations`), segments, profile writes, and wallet import — each gated on the matching `*:read`/`*:write` scope, with destructive tools requiring `"confirm": true`. + Use CLI command groups for operational resources: - `formo alerts` for alert list/get/create/update/delete/toggle/test From 1c5429330e1a9ddc2baef4d1df472540841e21d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 10:03:49 +0000 Subject: [PATCH 2/3] Track the trimmed public API surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to getformo/formono#2182 review decisions — the alert-test, contract-recommendations, contract-pipeline, and chart-meta routes are now internal-only (dashboard utilities), and GET /boards/{b}/charts is lazy by default with ?include=results: - Remove `alerts test`, `contracts recommendations`, and `contracts pipeline` (pipeline inclusion is set via `contracts update --include-in-pipeline`). - `charts list` returns summaries by default and gains `--results` to execute chart queries; `charts meta` now reads the list endpoint's summary default instead of the removed /charts/meta route. - Update the formo-analytics skill notes accordingly. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BWrVEx5C374qkrAQUCgx7y --- skills/formo-analytics/SKILL.md | 4 +- src/commands/alerts.ts | 69 +-------------------- src/commands/charts.ts | 25 ++++++-- src/commands/contracts.ts | 97 +----------------------------- test/commands/bodyBuilders.test.ts | 26 +------- 5 files changed, 25 insertions(+), 196 deletions(-) diff --git a/skills/formo-analytics/SKILL.md b/skills/formo-analytics/SKILL.md index 30ecfd9..fa6d820 100644 --- a/skills/formo-analytics/SKILL.md +++ b/skills/formo-analytics/SKILL.md @@ -87,11 +87,11 @@ Use `--timestamp ` with `profiles get` to return the stored wallet-enr ## Manage Formo resources -The MCP server exposes management tools for the same resources — alerts (including `test_alert`), boards/charts (including `reorder_charts` and `execute_saved_chart`), contracts (including `list_contract_recommendations`), segments, profile writes, and wallet import — each gated on the matching `*:read`/`*:write` scope, with destructive tools requiring `"confirm": true`. +The MCP server exposes management tools for the same resources — alerts, boards/charts (including `reorder_charts` and `execute_saved_chart`), contracts, segments, profile writes, and wallet import — each gated on the matching `*:read`/`*:write` scope, with destructive tools requiring `"confirm": true`. Use CLI command groups for operational resources: -- `formo alerts` for alert list/get/create/update/delete/toggle/test +- `formo alerts` for alert list/get/create/update/delete/toggle - `formo boards` and `formo charts` for dashboards and visualizations - `formo contracts` for tracked contract configuration and pipeline inclusion - `formo segments` for saved audiences diff --git a/src/commands/alerts.ts b/src/commands/alerts.ts index edaadd3..9b5e965 100644 --- a/src/commands/alerts.ts +++ b/src/commands/alerts.ts @@ -1,7 +1,7 @@ import { Cli, z } from 'incur' import { createClient, requireApiKey } from '../lib/client' import { isCanonicalFilterOperator } from '../lib/filters' -import { parseJsonArray, parseJsonObject } from '../lib/json' +import { parseJsonArray } from '../lib/json' import { buildPaginationParams, paginationOptionsSchema, @@ -262,70 +262,3 @@ alerts.command('toggle', { }, }) -// ── Test alert delivery ── - -export interface TestAlertOptions { - sampleEvent?: string - sampleUser?: string - recipientOverrides?: string -} - -export function buildTestAlertBody(options: TestAlertOptions) { - const body: Record = {} - if (options.sampleEvent !== undefined) { - body.sampleEvent = parseJsonObject(options.sampleEvent, '--sample-event') - } - if (options.sampleUser !== undefined) { - body.sampleUser = parseJsonObject(options.sampleUser, '--sample-user') - } - if (options.recipientOverrides !== undefined) { - body.recipientOverrides = parseJsonArray( - options.recipientOverrides, - '--recipient-overrides', - ) - } - return Object.keys(body).length > 0 ? body : undefined -} - -export function testAlertRun(alertId: string, options: TestAlertOptions = {}) { - requireApiKey() - const client = createClient() - return client.post( - `/v0/alerts/${encodeURIComponent(alertId)}/test`, - buildTestAlertBody(options), - ) -} - -alerts.command('test', { - description: 'Send a test delivery for an alert', - args: z.object({ - alertId: z.string().describe('Alert ID to test'), - }), - options: z.object({ - sampleEvent: z - .string() - .optional() - .describe('Optional JSON object to use as the sample event'), - sampleUser: z - .string() - .optional() - .describe('Optional JSON object to use as the sample user/profile'), - recipientOverrides: z - .string() - .optional() - .describe('Optional JSON array of recipient objects to test instead of saved recipients'), - }), - examples: [ - { - args: { alertId: 'alert_abc123' }, - options: { - sampleEvent: '{"event":"transaction","revenue":250}', - }, - description: 'Send a test alert with a sample event', - }, - ], - hint: 'Requires alerts:write scope on your API key.', - run({ args, options }) { - return testAlertRun(args.alertId, options) - }, -}) diff --git a/src/commands/charts.ts b/src/commands/charts.ts index 6385f7e..e866432 100644 --- a/src/commands/charts.ts +++ b/src/commands/charts.ts @@ -134,29 +134,43 @@ const chartBodyOptions = z.object({ export function listChartsRun( boardId: string, options: PaginationOptions = {}, + results = false, ) { requireApiKey() const client = createClient() return client.get(`/v0/boards/${encodeURIComponent(boardId)}/charts/`, { - params: buildPaginationParams(options), + params: { + ...buildPaginationParams(options), + // The public API returns lightweight summaries unless asked to execute. + ...(results ? { include: 'results' } : {}), + }, }) } charts.command('list', { - description: 'List all charts for a board, including executed results', + description: + 'List charts for a board (summaries by default; --results executes each chart query)', options: z.object({ boardId: z.string().describe('Board ID to list charts from'), + results: z + .boolean() + .optional() + .describe('Execute each chart query and include results (slower)'), ...paginationOptionsSchema, }), examples: [ { options: { boardId: 'board_abc123' }, - description: 'List all charts in a board', + description: 'List chart summaries for a board', + }, + { + options: { boardId: 'board_abc123', results: true }, + description: 'List charts with executed query results', }, ], hint: 'Requires boards:read scope on your API key.', run({ options }) { - return listChartsRun(options.boardId, options) + return listChartsRun(options.boardId, options, options.results ?? false) }, }) @@ -168,7 +182,8 @@ export function listChartSummariesRun( ) { requireApiKey() const client = createClient() - return client.get(`/v0/boards/${encodeURIComponent(boardId)}/charts/meta`, { + // Summaries are the list endpoint's default; /charts/meta is internal-only. + return client.get(`/v0/boards/${encodeURIComponent(boardId)}/charts/`, { params: buildPaginationParams(options), }) } diff --git a/src/commands/contracts.ts b/src/commands/contracts.ts index 404c42e..86abb90 100644 --- a/src/commands/contracts.ts +++ b/src/commands/contracts.ts @@ -11,7 +11,7 @@ export type { PaginationOptions } export const contracts = Cli.create('contracts', { description: - 'Smart contract commands — register, list, recommend, update, toggle pipeline inclusion, and remove tracked contracts', + 'Smart contract commands — register, list, update, and remove tracked contracts', }) function parseChain(chain: string | number) { @@ -52,50 +52,6 @@ export function getContractRun(chain: string, address: string) { ) } -// ── Recommended contracts ── - -export function getContractRecommendationsRun() { - requireApiKey() - const client = createClient() - return client.get('/v0/contracts/recommendations') -} - -contracts.command('recommendations', { - description: - 'List contracts the project already interacts with but has not added yet', - options: z.object({}), - examples: [ - { - description: 'Show recommended contracts to add for decoding/monitoring', - }, - ], - hint: 'Requires contracts:read scope on your API key.', - run() { - return getContractRecommendationsRun() - }, -}) - -contracts.command('get', { - description: 'Get a tracked contract by chain and address', - args: z.object({ - chain: z.string().describe('Chain ID'), - address: z.string().describe('Contract address (0x...)'), - }), - examples: [ - { - args: { - chain: '1', - address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - }, - description: 'Get a tracked USDC contract', - }, - ], - hint: 'Requires contracts:read scope on your API key.', - run({ args }) { - return getContractRun(args.chain, args.address) - }, -}) - // ── Create a contract ── export interface CreateContractOptions { @@ -243,57 +199,6 @@ contracts.command('update', { }, }) -// ── Toggle contract pipeline inclusion ── - -export function updateContractPipelineRun( - chain: string, - address: string, - includeInPipeline: boolean, -) { - requireApiKey() - const client = createClient() - return client.patch( - `/v0/contracts/${parseChain(chain)}/${encodeURIComponent(address)}/pipeline`, - buildUpdateContractPipelineBody(includeInPipeline), - ) -} - -export function buildUpdateContractPipelineBody(includeInPipeline: boolean) { - return { include_in_pipeline: includeInPipeline } -} - -contracts.command('pipeline', { - description: - 'Toggle whether a tracked contract is included in the project events pipeline', - args: z.object({ - chain: z.string().describe('Chain ID'), - address: z.string().describe('Contract address (0x...)'), - }), - options: z.object({ - includeInPipeline: z - .boolean() - .describe('true to include the contract in the pipeline, false to exclude it'), - }), - examples: [ - { - args: { - chain: '1', - address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - }, - options: { includeInPipeline: false }, - description: 'Keep ABI decoding but exclude this contract from pipeline deploys', - }, - ], - hint: 'Requires contracts:write scope on your API key.', - run({ args, options }) { - return updateContractPipelineRun( - args.chain, - args.address, - options.includeInPipeline, - ) - }, -}) - // ── Delete a contract ── export function deleteContractRun(chain: string, address: string) { diff --git a/test/commands/bodyBuilders.test.ts b/test/commands/bodyBuilders.test.ts index fd5de18..249847c 100644 --- a/test/commands/bodyBuilders.test.ts +++ b/test/commands/bodyBuilders.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { buildAlertBody, buildTestAlertBody } from '../../src/commands/alerts'; +import { buildAlertBody } from '../../src/commands/alerts'; import { buildBoardBody } from '../../src/commands/boards'; import { buildChartBody, @@ -7,7 +7,6 @@ import { } from '../../src/commands/charts'; import { buildCreateContractBody, - buildUpdateContractPipelineBody, buildUpdateContractBody, } from '../../src/commands/contracts'; import { buildImportBody } from '../../src/commands/import'; @@ -106,21 +105,6 @@ describe('commands / body builders', function () { }); }); - describe('buildTestAlertBody()', function () { - it('parses sample objects and recipient overrides', function () { - const body = buildTestAlertBody({ - sampleEvent: '{"event":"transaction"}', - sampleUser: '{"address":"0xabc"}', - recipientOverrides: '[{"type":"email","value":["a@b.com"]}]', - }); - expect(body).to.deep.equal({ - sampleEvent: { event: 'transaction' }, - sampleUser: { address: '0xabc' }, - recipientOverrides: [{ type: 'email', value: ['a@b.com'] }], - }); - }); - }); - // ── Boards ── describe('buildBoardBody()', function () { @@ -240,14 +224,6 @@ describe('commands / body builders', function () { }); }); - describe('buildUpdateContractPipelineBody()', function () { - it('maps includeInPipeline to include_in_pipeline', function () { - expect(buildUpdateContractPipelineBody(false)).to.deep.equal({ - include_in_pipeline: false, - }); - }); - }); - // ── Segments ── describe('buildCreateSegmentBody()', function () { From 071e74982e77e8c1436eecde945db914390d9646 Mon Sep 17 00:00:00 2001 From: Yos Riady Date: Thu, 6 Aug 2026 18:33:30 +0700 Subject: [PATCH 3/3] Restore the contracts get command dropped by the surface trim The hunk removing the recommendations block also took the get registration with it. GET /v0/contracts/{chain}/{address} is still a public route, so the command comes back verbatim from main. Co-Authored-By: Claude Fable 5 --- src/commands/contracts.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/commands/contracts.ts b/src/commands/contracts.ts index 86abb90..59bc905 100644 --- a/src/commands/contracts.ts +++ b/src/commands/contracts.ts @@ -52,6 +52,27 @@ export function getContractRun(chain: string, address: string) { ) } +contracts.command('get', { + description: 'Get a tracked contract by chain and address', + args: z.object({ + chain: z.string().describe('Chain ID'), + address: z.string().describe('Contract address (0x...)'), + }), + examples: [ + { + args: { + chain: '1', + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }, + description: 'Get a tracked USDC contract', + }, + ], + hint: 'Requires contracts:read scope on your API key.', + run({ args }) { + return getContractRun(args.chain, args.address) + }, +}) + // ── Create a contract ── export interface CreateContractOptions {