diff --git a/skills/formo-analytics/SKILL.md b/skills/formo-analytics/SKILL.md index d463114..fa6d820 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,9 +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, 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..59bc905 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,29 +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({ @@ -243,57 +220,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 () {