diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ff0de9b..3fe135b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,12 +9,6 @@ "version": "1.0.0" }, "plugins": [ - { - "name": "major", - "source": "./plugins/major", - "description": "Use the Major platform agentically via Claude Code", - "version": "1.0.7" - }, { "name": "major-build", "source": "./plugins/major-build", diff --git a/plugins/major-build/skills/agent-builder/SKILL.md b/plugins/major-build/skills/agent-builder/SKILL.md index 53f6f46..4e8fea4 100644 --- a/plugins/major-build/skills/agent-builder/SKILL.md +++ b/plugins/major-build/skills/agent-builder/SKILL.md @@ -67,9 +67,9 @@ On Slack there is no panel. Tell the user to open the agent in the web app to fi ## Picking connectors and applications - Use `mcp__plugin_major-build_major__execute_resource_tool` with `toolName: "mcp__resources__list_resources"` to list the org's connectors; `mcp__plugin_major-build_major__list_apps` lists attachable apps. **Use `list_apps`, not `list_use_apps`** — an agent can only be granted apps the user can edit. -- If no existing connector matches, call `mcp__interactions__request_resource_setup` to prompt the user to create one inline. `connectorId` is required — pass one you already know (e.g. `"postgresql"`, `"snowflake"`) or use `mcp__plugin_major-build_major__execute_resource_tool` with `toolName: "mcp__resources__search_connector_types"` to discover the connectors you can set up; ask if unsure. The tool blocks until the user finishes or declines; on success add the returned `resourceId` to `connectors` in `agent.jsonc`. +- If no existing connector matches, call `mcp__plugin_major-build_major__request_resource_setup` to prompt the user to create one inline. `connectorId` is required — pass one you already know (e.g. `"postgresql"`, `"snowflake"`) or use `mcp__plugin_major-build_major__execute_resource_tool` with `toolName: "mcp__resources__search_connector_types"` to discover the connectors you can set up; ask if unsure. The tool blocks until the user finishes or declines; on success add the returned `resourceId` to `connectors` in `agent.jsonc`. - Slack is provisioned automatically when the user installs the Major Slack integration (Settings → Integrations) and is intentionally not a creatable connector — if it's missing from `list_resources`, tell them to install the integration. -- If an existing connector needs more configuration to be usable (e.g. selecting a Google Sheets spreadsheet), call `mcp__interactions__request_resource_update` with the `resourceId` and what's missing. +- If an existing connector needs more configuration to be usable (e.g. selecting a Google Sheets spreadsheet), call `mcp__plugin_major-build_major__request_resource_update` with the `resourceId` and what's missing. - After adding skills, call `list_suggested_connectors` — it returns connectors the attached skills' scripts actually use that the agent can't access yet. Propose them to the user and add accepted ones to `connectors`; a skill whose connector is missing will fail at runtime. Entries with `canAdd=false` need access the current user doesn't have — tell them to ask an admin. - Don't add connectors or applications speculatively — every one expands the agent's permissions. Keep the set minimal. diff --git a/plugins/major-build/skills/app-builder/SKILL.md b/plugins/major-build/skills/app-builder/SKILL.md index bca0183..b10711e 100644 --- a/plugins/major-build/skills/app-builder/SKILL.md +++ b/plugins/major-build/skills/app-builder/SKILL.md @@ -96,4 +96,4 @@ Use each command's `--help` for filters and pagination. For app secrets, use the available MCP setup tool: `set-app-env-variables` in app chats, or `set_env_variables` on the build server. The user supplies values through the frontend; never ask them to paste secrets into chat. If the tool returns a setup URL, share it in one short sentence and wait for the user's confirmation. Use `major vars set KEY=VALUE` only for known values the user explicitly wants you to configure. -New connector setup still uses `mcp__interactions__request_resource_setup`; load `using-connectors` for that flow. An existing connector can be added to app code separately. +New connector setup still uses `mcp__plugin_major-build_major__request_resource_setup`; load `using-connectors` for that flow. An existing connector can be added to app code separately. diff --git a/plugins/major-build/skills/using-connectors/references/github.md b/plugins/major-build/skills/using-connectors/references/github.md index e714466..a7eff69 100644 --- a/plugins/major-build/skills/using-connectors/references/github.md +++ b/plugins/major-build/skills/using-connectors/references/github.md @@ -4,7 +4,7 @@ GitHub uses a GitHub App installation. When the user asks to connect GitHub: -1. Call `mcp__interactions__request_resource_setup` with `connectorId: "github"`. +1. Call `mcp__plugin_major-build_major__request_resource_setup` with `connectorId: "github"`. 2. Ask the user to finish the GitHub installation flow and select the repositories the app may access. 3. After setup completes, call `mcp__resources__list_resources` and use the connected GitHub resource's `resourceId` and mounted MCP slug. diff --git a/plugins/major-build/skills/using-connectors/references/gmail.md b/plugins/major-build/skills/using-connectors/references/gmail.md index d5e0a8b..dc6e41f 100644 --- a/plugins/major-build/skills/using-connectors/references/gmail.md +++ b/plugins/major-build/skills/using-connectors/references/gmail.md @@ -6,7 +6,7 @@ Gmail requires OAuth authentication before use. ### When the user asks you to set up Gmail or connect their email: -1. Call `mcp__interactions__request_resource_setup` with `connectorId: "gmail"` — this prompts the user to authenticate with Google +1. Call `mcp__plugin_major-build_major__request_resource_setup` with `connectorId: "gmail"` — this prompts the user to authenticate with Google 2. Once setup completes, the resource is ready to use --- diff --git a/plugins/major-build/skills/using-connectors/references/googlecalendar.md b/plugins/major-build/skills/using-connectors/references/googlecalendar.md index 3e67894..37c87b8 100644 --- a/plugins/major-build/skills/using-connectors/references/googlecalendar.md +++ b/plugins/major-build/skills/using-connectors/references/googlecalendar.md @@ -6,7 +6,7 @@ Google Calendar requires OAuth authentication before use. ### When the user asks you to set up Google Calendar or connect their calendar: -1. Call `mcp__interactions__request_resource_setup` with `connectorId: "googlecalendar"` — this prompts the user to authenticate with Google +1. Call `mcp__plugin_major-build_major__request_resource_setup` with `connectorId: "googlecalendar"` — this prompts the user to authenticate with Google 2. Once setup completes, the resource is ready to use --- diff --git a/plugins/major-build/skills/using-connectors/references/googledrive.md b/plugins/major-build/skills/using-connectors/references/googledrive.md index 2c2beab..293ff9e 100644 --- a/plugins/major-build/skills/using-connectors/references/googledrive.md +++ b/plugins/major-build/skills/using-connectors/references/googledrive.md @@ -6,7 +6,7 @@ Google Drive requires OAuth authentication before use. ### When the user asks you to set up Google Drive or connect their files: -1. Call `mcp__interactions__request_resource_setup` with `connectorId: "googledrive"` — this prompts the user to authenticate with Google +1. Call `mcp__plugin_major-build_major__request_resource_setup` with `connectorId: "googledrive"` — this prompts the user to authenticate with Google 2. Once setup completes, the resource is ready to use --- diff --git a/plugins/major-build/skills/using-connectors/references/googlesheets.md b/plugins/major-build/skills/using-connectors/references/googlesheets.md index b1b7263..125a7f8 100644 --- a/plugins/major-build/skills/using-connectors/references/googlesheets.md +++ b/plugins/major-build/skills/using-connectors/references/googlesheets.md @@ -6,8 +6,8 @@ Google Sheets requires a two-step setup: (1) OAuth authentication, (2) spreadshe ### When the user asks you to set up Google Sheets or connect a spreadsheet: -1. Call `mcp__interactions__request_resource_setup` with `connectorId: "googlesheets"` — this prompts the user to authenticate with Google -2. After setup completes, call `mcp__interactions__request_resource_update` with the returned `resourceId` and `message: "Please select your spreadsheet. Click 'Configure Resource' below, then use the spreadsheet picker to choose your sheet."` — this prompts them to select their spreadsheet +1. Call `mcp__plugin_major-build_major__request_resource_setup` with `connectorId: "googlesheets"` — this prompts the user to authenticate with Google +2. After setup completes, call `mcp__plugin_major-build_major__request_resource_update` with the returned `resourceId` and `message: "Please select your spreadsheet. Click 'Configure Resource' below, then use the spreadsheet picker to choose your sheet."` — this prompts them to select their spreadsheet 3. Once both steps complete, the resource is ready to use ### When the user sends a Google Sheets link: @@ -20,7 +20,7 @@ If the user shares a Google Sheets URL (e.g., `https://docs.google.com/spreadshe ### When a Google Sheets resource exists but has no spreadsheet selected: -If you call a Google Sheets MCP tool and get an error indicating no spreadsheet is configured, use `mcp__interactions__request_resource_update` to prompt the user to select one. +If you call a Google Sheets MCP tool and get an error indicating no spreadsheet is configured, use `mcp__plugin_major-build_major__request_resource_update` to prompt the user to select one. --- diff --git a/plugins/major-build/skills/using-connectors/references/managed-file-storage.md b/plugins/major-build/skills/using-connectors/references/managed-file-storage.md index a96cff6..e356cee 100644 --- a/plugins/major-build/skills/using-connectors/references/managed-file-storage.md +++ b/plugins/major-build/skills/using-connectors/references/managed-file-storage.md @@ -10,7 +10,7 @@ Customers see a flat key namespace (e.g. `user/avatar.png`); the underlying buck ## Setting It Up -Managed file storage is **not** offered through `mcp__interactions__request_resource_setup` — that tool only covers connectors set up via the standard Add-Connector dialog, and file storage is provisioned differently. Do not try to set it up that way; it will not appear. Use the dedicated tools instead: +Managed file storage is **not** offered through `mcp__plugin_major-build_major__request_resource_setup` — that tool only covers connectors set up via the standard Add-Connector dialog, and file storage is provisioned differently. Do not try to set it up that way; it will not appear. Use the dedicated tools instead: - `mcp__resources__list_managed_file_stores` — list existing file stores in the org. **Always call this first** — reuse an existing store if one fits the use case. - `mcp__resources__provision_managed_file_store` — create a new org-level file store. Synchronous; returns `{ resourceId, name }` immediately. Args: `name`. The caller is auto-granted `Resource:Admin`; the All Builders group gets `Resource:Builder`, so any builder in the org can use it. diff --git a/plugins/shared/skills/authn/SKILL.md b/plugins/shared/skills/authn/SKILL.md deleted file mode 100644 index 46d344c..0000000 --- a/plugins/shared/skills/authn/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: using-authn -description: Implements AuthN (identity) operations. Use whenever building anything that requires AuthN ---- - -To get a user's identity, use @lib/auth.ts. If the user is logged in, the endpoint will return the user's email, userId, and name. - -All AuthZ should be handled by the user's application. This function should be purely used for identifying the current user that is logged in. There should be no other way of determining the user's identity. diff --git a/plugins/shared/skills/crons/SKILL.md b/plugins/shared/skills/crons/SKILL.md deleted file mode 100644 index c78e5ae..0000000 --- a/plugins/shared/skills/crons/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: using-crons -description: Use when the user needs to run something on a cadence or do anything that interacts with crons or scheduled jobs. Scheduled work runs through Major workflows — a cron trigger calling a route on the deployed app. ---- - -# Scheduled Jobs (Workflows) - -Scheduled work on Major is handled by **workflows**: graphs of steps run by the platform's workflow engine. A scheduled job is a workflow with a **cron trigger** and an **`app_call` step** that calls a route on the deployed app on a cadence. - -The legacy `cron.json` system is retired — the file is no longer read on deploy, and existing crons were migrated to workflows automatically. If the project still has a `cron.json`, it has no effect and can be deleted. - -## Building a scheduled job - -Both halves are yours to build: - -1. **The route** — implement an HTTP route in the app that performs the work (see below), and deploy it: workflows call the deployed app. -2. **The workflow** — invoke the **`workflow-builder`** skill and follow it to build the workflow itself: create the workflow, define a cron trigger and an `app_call` node targeting the route's method and path, then save, test, and publish through that skill's lifecycle. - -## The route - -```typescript -// app/api/jobs/cleanup/route.ts -export async function POST() { - // ... remove expired sessions - return Response.json({ ok: true }); -} -``` - -How workflow calls reach the route: - -- Calls go to the **deployed** app, so the app must be deployed before the workflow can call the route. -- Requests arrive authenticated as the Major user the workflow runs as (via a platform-signed app-call JWT) — the route needs no webhook access and the app does not need to be public. -- The step's configured input arrives as query params for GET/DELETE and as a JSON body for POST/PUT/PATCH. -- The response body becomes the step's output, available to later steps in the workflow. - -## Cron schedules - -Cron triggers use a 5-field cron expression (`minute hour day-of-month month day-of-week` — no seconds field) with an IANA timezone. Common schedules: - -| Schedule | Expression | -| ---------------------- | ------------- | -| Every 5 minutes | `*/5 * * * *` | -| Every hour | `0 * * * *` | -| Every day at 2 AM | `0 2 * * *` | -| Every Monday 9 AM | `0 9 * * 1` | -| 1st of month, midnight | `0 0 1 * *` | diff --git a/plugins/shared/skills/data-table/SKILL.md b/plugins/shared/skills/data-table/SKILL.md deleted file mode 100644 index 28ca29c..0000000 --- a/plugins/shared/skills/data-table/SKILL.md +++ /dev/null @@ -1,1252 +0,0 @@ ---- -name: using-frontend-data-table -description: Creates frontend table views. Use whenever creating any type of table UI on the frontend. Use when doing ANYHTHING that involves tables (using tables to display data). ---- - -# DataTable - -A composable, full-featured DataTable built on TanStack Table v8. Installed via the shadcn registry — code lives locally in the project. - -## Installation - -Install via the shadcn registry: - -```bash -npx shadcn@latest add https://cdn.major.build/shadcn/data-table.json -``` - -This copies the data table source code into `components/data-table/` in the project. All imports use the project's path alias (typically `@/components/data-table`). - -## Is a Table the Right UI? - -Before building a table, evaluate whether the data actually fits a tabular layout. Check these signals: - -**Use a table when:** - -- Each record has 3+ distinct fields (e.g. name, email, role, status, created date) -- Users need to compare values across rows (scanning columns) -- The data benefits from sorting, filtering, or pagination -- The page is operational — users manage, edit, or act on records - -**Don't use a table when:** - -- Items are media-heavy (images, thumbnails, previews are the primary content) -- Each item has only 1–2 fields — a simple list or card grid is better -- The layout is a feed, timeline, or activity log with variable-length content -- Visual presentation matters more than data density (e.g. product showcase, profile cards) - -**When ambiguous** (e.g. "show me a list of projects" — could be cards or a table), use `AskUserQuestion` to ask the user whether they want a data table or a card/list layout. Briefly explain the trade-off: tables are better for dense, scannable data with actions; cards are better for visual, media-rich items. - -If a table is not the right fit, do not use this skill — build the appropriate UI directly instead. - -## Feature Selection - -Every table includes these features by default — do not ask the user about them: - -- **Global search** — Search across all columns with debounced input -- **Column filters** — Dynamic per-column filters (select, text, number, date, boolean) -- **Column sorting** — Click column headers to sort - -Before implementing a table, use the `AskUserQuestion` tool to ask the user which additional features they need. - -First, ask a single-select question for the pagination strategy: - -- **Offset pagination** — Page navigation with page size selector -- **Infinite scroll** — Load more rows automatically as the user scrolls -- **None** — No pagination (all data rendered at once) - -Then, ask a multi-select question for optional features: - -- **Column visibility** — Toggle which columns are visible -- **Column reordering** — Drag-and-drop to rearrange columns -- **Column resizing** — Drag column borders to resize -- **Row actions** — Three-dot dropdown menu per row (edit, delete, etc.) -- **Row selection** — Checkbox selection with bulk action bar -- **Expandable rows** — Click to expand rows with detail content -- **Data export** — Export current page or all data as CSV -- **Sticky header** — Pin header while scrolling the table body - -Only compose the features the user selects. Do not add features they did not ask for. If the user rejects or dismisses the question, infer the most reasonable set of features from context (the data source, dataset size, and what the user described) and proceed without asking again. - -**Virtualization:** Do not ask the user about virtualization. Enable it automatically when the page size exceeds 200 rows or when infinite scroll is selected, since those scenarios render enough rows to benefit from it. - -## Version Check - -When the user asks to modify, update, or add features to an existing table, check whether the installed version is current before making changes: - -1. Read the local `DATA_TABLE_VERSION` constant from the project's `data-table/constants.ts`. -2. Fetch `https://cdn.major.build/shadcn/data-table.json` and read the `version` field from the JSON. -3. If the remote version is newer than the local version, inform the user and suggest updating: - - Run `npx shadcn@latest add https://cdn.major.build/shadcn/data-table.json` to pull the latest code. - - If the user confirms the update, verify that any existing backend endpoints (API routes, resource queries) still work correctly with the updated table code. Fix any breaking changes. - - Review the changelog between versions. If the new version introduced features that are relevant to the user's table, suggest incorporating them using `AskUserQuestion`. Only suggest features that make sense for their use case — do not push everything. -4. If versions match, proceed directly with the requested changes. - -## Quick Start — Auto Mode - -Pass `onLoadRows` and the table manages ALL internal state — data, loading, sorting, filtering, pagination. - -```tsx -import { DataTable, DataTableContent, DataTablePagination, buildRequestSearchParams } from "@/components/data-table"; -import type { ColumnDef, DataTableResponse } from "@/components/data-table"; - -interface User { - id: string; - name: string; - email: string; -} - -const columns: ColumnDef[] = [ - { accessorKey: "name", header: "Name" }, - { accessorKey: "email", header: "Email" }, -]; - -async function loadUsers(params) { - const res = await fetch(`/api/users?${buildRequestSearchParams(params)}`); - if (!res.ok) { - return { success: false, error: { code: String(res.status), message: "Failed to fetch" } }; - } - return res.json(); // Must return DataTableResponse -} - -export default function UsersPage() { - return ( - - - - - ); -} -``` - -## Architecture - -**Compound components via context.** The root `` creates a TanStack Table instance and provides it through React Context. Sub-components consume the context — compose only the pieces you need. - -**Two modes:** - -1. **Auto mode** — Pass `onLoadRows`. The table calls it with `DataTableRequestParams` whenever sorting, filtering, or pagination changes. All data fetching and state management is handled internally. -2. **Static mode** — Pass `data` directly. Sorting, filtering, and pagination happen client-side by default. Optionally pass controlled state props (`sorting` + `onSortingChange`, etc.) for server-controlled static mode. - -**Composable.** Every sub-component is optional. Mix and match ``, ``, ``, ``, etc. - ---- - -## Composition Guide - -### Minimal table — just content - -```tsx - - - -``` - -### With pagination - -```tsx - - - - -``` - -### With infinite scroll - -```tsx - - - - -``` - -### With toolbar (search + filters + column toggle + export) - -```tsx - - - - -
- rowsToCsv(rows)} - onExportAll={async () => downloadFile("/api/users/export")} - /> - - - - - -``` - -### With row selection + bulk actions - -```tsx -(), ...baseColumns]} getRowId={(u) => u.id}> - > - {(rows) => } - - - - -``` - -### With row actions - -```tsx -const columns = [ - ...baseColumns, - createActionsColumn((row) => [ - { label: "Edit", onSelect: (r) => router.push(`/users/${r.id}/edit`) }, - { label: "View details", onSelect: (r) => router.push(`/users/${r.id}`) }, - { type: "separator" }, - { - label: "Delete", - variant: "destructive", - onSelect: async (r, { removeRow }) => { - await handleDelete(r.id); - removeRow(r.id); - }, - }, - ]), -]; - - - - -; -``` - -### With expandable rows - -```tsx -(), ...baseColumns]}> - } /> - - -``` - -### With virtualization (large datasets) - -Virtualization renders only visible rows. Requires `maxHeight` to define the scroll viewport. Built-in infinite scroll triggers `loadNextPage` automatically when nearing the bottom. - -```tsx - - - -``` - -### With sticky header - -```tsx - - - - -``` - -### Kitchen sink - -```tsx -const columns = [ - createSelectColumn(), - createExpandColumn(), - ...baseColumns, - createActionsColumn((row) => [ - { label: "Edit", onSelect: (r) => router.push(`/users/${r.id}/edit`) }, - { type: "separator" }, - { - label: "Delete", - variant: "destructive", - onSelect: async (r, { removeRow }) => { - await api.deleteUser(r.id); - removeRow(r.id); - }, - }, - ]), -]; - - u.id} - pageSize={50} - enableColumnReordering - enableColumnResizing - onError={(err) => toast.error(err.message)} -> - - - -
- rowsToCsv(rows)} - onExportAll={async () => downloadFile("/api/users/export")} - /> - - - > - {(rows) => } - - } stickyHeader maxHeight={600} /> - -; -``` - -**Note:** The actions column is automatically pinned to the right edge with a sticky position. When the table overflows horizontally, the actions column stays visible with a subtle left shadow. It is excluded from column drag-and-drop reordering. - -### Static mode — client-side data - -All sorting, filtering, and pagination happens in the browser. No server calls. - -```tsx - - - - - - - - -``` - -### Static mode with server-controlled state - -Pass `data` but also provide controlled state props. The table switches to manual mode when it sees `sorting + onSortingChange`, `pagination + onPaginationChange`, etc. - -```tsx -const [sorting, setSorting] = useState([]); -const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); - -// Fetch data whenever sorting/pagination changes -const { data, totalCount, isLoading } = useFetchUsers({ sorting, pagination }); - - - - -; -``` - ---- - -## Component Reference - -### `` — Root - -Creates the TanStack Table instance and provides context. All sub-components must be children. - -**Auto mode props** (pass `onLoadRows`): - -| Prop | Type | Default | Description | -| ------------- | ---------------------------------------------------- | ------- | ------------------------------------------------------------------------ | -| `onLoadRows` | `DataTableLoadRowsFn` | — | Promise-returning load function — enables auto mode | -| `initialData` | `PaginatedResponse` | — | Pre-loaded first page for SSR — renders immediately, skips initial fetch | -| `onError` | `(error: { code: string; message: string }) => void` | — | Called when `onLoadRows` returns an error response | - -**Static mode props** (pass `data`): - -| Prop | Type | Default | Description | -| ----------------------------------------- | ----------------------------------- | ------- | ------------------------------------------- | -| `data` | `TData[]` | — | Row data to display | -| `isLoading` | `boolean` | `false` | Show loading state | -| `sorting` / `onSortingChange` | `SortingState` / `OnChangeFn` | — | Controlled sorting (implies manual mode) | -| `columnFilters` / `onColumnFiltersChange` | `ColumnFiltersState` / `OnChangeFn` | — | Controlled filtering (implies manual mode) | -| `globalFilter` / `onGlobalFilterChange` | `string` / `OnChangeFn` | — | Controlled search | -| `pagination` / `onPaginationChange` | `PaginationState` / `OnChangeFn` | — | Controlled pagination (implies manual mode) | -| `rowCount` | `number` | — | Total row count for server-side pagination | - -**Shared props** (both modes): - -| Prop | Type | Default | Description | -| ----------------------------------------------- | ---------------------------------- | ------- | --------------------------------- | -| `columns` | `ColumnDef[]` | — | Column definitions (required) | -| `getRowId` | `(row: TData) => string` | — | Custom row ID extractor | -| `pageSize` | `number` | `50` | Initial page size | -| `rowSelection` / `onRowSelectionChange` | `RowSelectionState` / `OnChangeFn` | — | Controlled row selection | -| `expanded` / `onExpandedChange` | `ExpandedState` / `OnChangeFn` | — | Controlled expanded rows | -| `columnVisibility` / `onColumnVisibilityChange` | `VisibilityState` / `OnChangeFn` | — | Controlled column visibility | -| `enableMultiSort` | `boolean` | `false` | Allow sorting by multiple columns | -| `enableColumnReordering` | `boolean` | `false` | Drag-and-drop column reordering | -| `enableColumnResizing` | `boolean` | `false` | Drag column borders to resize | - -### `` — Main table - -Renders the full table: header, body, loading skeleton, empty state, and error state with retry. - -| Prop | Type | Default | Description | -| ------------------- | -------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| `renderExpandedRow` | `(row: Row) => ReactNode` | — | Content to show when a row is expanded | -| `onRowClick` | `(row: Row) => void` | — | Click handler for rows | -| `emptyTitle` | `string` | `"No results"` | Title for empty state | -| `emptyDescription` | `string` | `"No data to display."` | Description for empty state | -| `emptyContent` | `ReactNode` | — | Custom empty state content | -| `stickyHeader` | `boolean` | `false` | Pin the header while scrolling. Pair with `maxHeight`. | -| `maxHeight` | `number \| string` | — | Constrains the scroll area height. Required for `stickyHeader` and `virtualized`. | -| `virtualized` | `boolean` | `false` | Enable row virtualization. Renders only visible rows + overscan buffer. Built-in infinite scroll triggers `loadNextPage` when nearing the bottom. | -| `estimateRowHeight` | `number` | `48` | Estimated row height in px for the virtualizer | -| `overscan` | `number` | `5` | Number of rows to render outside the visible area | - -### `` — Offset pagination - -| Prop | Type | Default | Description | -| ----------------- | ---------- | ------------------- | --------------------------------- | -| `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | Page size choices | -| `sticky` | `boolean` | `false` | Pin to bottom of scroll container | - -### `` — Infinite scroll - -In auto mode, no props needed — reads `loadNextPage` and `hasMore` from context. Uses IntersectionObserver on a sentinel element. - -| Prop | Type | Description | -| ------------ | ----------------------------- | ------------------------------------- | -| `onLoadMore` | `() => void \| Promise` | Manual mode: function to load more | -| `hasMore` | `boolean` | Manual mode: whether more data exists | - -### `` — Toolbar container - -Simple flex container for composing toolbar items. Props: `className`, `children`. - -### `` — Global search - -| Prop | Type | Default | Description | -| ------------- | -------- | ------------- | ---------------- | -| `placeholder` | `string` | `"Search..."` | Placeholder text | -| `debounceMs` | `number` | `300` | Debounce delay | - -### `` — Dynamic column filters - -Renders a "Filters" button that opens a dropdown where users dynamically add/remove column filters. Each filter type has a purpose-built inline input. - -| Prop | Type | Description | -| --------- | ----------------------------- | ------------------------------ | -| `filters` | `DataTableFilterDefinition[]` | Filter definitions (see below) | - -**Filter definition types:** - -```tsx -import type { DataTableFilterDefinition } from "@/components/data-table"; - -const filters: DataTableFilterDefinition[] = [ - { - columnId: "role", - title: "Role", - type: "select", - options: [ - { label: "Admin", value: "admin" }, - { label: "Editor", value: "editor" }, - ], - }, - { columnId: "name", title: "Name", type: "text" }, - { columnId: "score", title: "Score", type: "number" }, - { columnId: "createdAt", title: "Created", type: "date" }, - { columnId: "verified", title: "Verified", type: "boolean" }, -]; -``` - -**Filter types, operators, and query param encoding:** - -| Type | Operators | Column filter value shape | URL params | -| --------- | --------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `select` | (multi-select, no operator) | `string[]` | `role=admin,editor` | -| `text` | `contains`, `eq`, `starts_with`, `ends_with`, `neq` | `{ op, value }` | `name=john&name_op=contains` | -| `number` | `eq`, `lt`, `gt`, `neq`, `range` | `{ op, value }` or `{ op, min, max }` | `score=50&score_op=gt` or `score_op=range&score_min=10&score_max=100` | -| `date` | `eq`, `before`, `after`, `neq`, `range` | `{ op, value }` or `{ op, from, to }` | `createdAt=2024-06-01&createdAt_op=before` or `createdAt_op=range&createdAt_from=...&createdAt_to=...` | -| `boolean` | (toggle, no operator) | `boolean` | `verified=true` | - -### `` — Standalone column filter - -For cases where you need a standalone filter button for one column (outside the dynamic filter system). - -| Prop | Type | Description | -| ---------- | ------------------------------------ | ----------------- | -| `columnId` | `string` | Column to filter | -| `title` | `string` | Button label | -| `options` | `{ label: string; value: string }[]` | Options to select | - -### `` — Column visibility dropdown - -| Prop | Type | Default | Description | -| ------- | -------- | ----------- | ------------ | -| `label` | `string` | `"Columns"` | Button label | - -### `` — Export button - -Supports current-page export (client-side) and all-data export (server-side). When `onExportAll` is provided, renders a dropdown with "Current page" and "All data" options. - -| Prop | Type | Default | Description | -| ------------- | ----------------------------- | -------------- | ------------------------------------------------------------------- | -| `formatRows` | `(rows: TData[]) => string` | — | Serializes current-page row data to a file string (e.g. CSV) | -| `filename` | `string` | `"export.csv"` | Filename for current-page export | -| `onExportAll` | `() => void \| Promise` | — | Calls a server endpoint for full export. Use with `downloadFile()`. | -| `label` | `string` | `"Export"` | Button label | - -### `` — Bulk action bar - -Renders only when at least one row is selected. - -| Prop | Type | Description | -| ---------- | ------------------------------------------- | ----------------------------------- | -| `children` | `(selectedRows: Row[]) => ReactNode` | Render prop receiving selected rows | - -### `` — Loading skeleton - -| Prop | Type | Default | Description | -| ------------- | -------- | ------- | -------------------------- | -| `columnCount` | `number` | `4` | Number of skeleton columns | -| `rowCount` | `number` | `5` | Number of skeleton rows | - -### `` — Empty state - -| Prop | Type | Default | Description | -| ------------- | ----------- | ----------------------- | ----------------------- | -| `title` | `string` | `"No results"` | Empty state title | -| `description` | `string` | `"No data to display."` | Empty state description | -| `children` | `ReactNode` | — | Custom content | - ---- - -## Column Reordering & Resizing - -### Column reordering - -Enable with `enableColumnReordering` on ``. Users drag columns by a grip handle to rearrange them. The new order is applied via TanStack Table's `setColumnOrder`. - -```tsx - - - - -``` - -**Non-draggable columns:** The select (`_select`), expand (`_expand`), and actions (`_actions`) columns are excluded from reordering. They stay pinned in their positions. - -**Controlled column order:** To persist or control the order externally, use TanStack Table's `columnOrder` state: - -```tsx -const [columnOrder, setColumnOrder] = useState([]); - - - -; -``` - -### Column resizing - -Enable with `enableColumnResizing` on ``. Users drag the right border of column headers to resize. A blue indicator appears on hover/drag. Body cells also have resize handles for convenience. - -```tsx - - - - -``` - -**How it works:** On first render with data, the table measures actual DOM column widths and pre-populates TanStack's `columnSizing` state. This prevents a layout jump when the user starts resizing. Once measured, the table switches to `table-layout: fixed` with explicit widths. - -**Opting out per column:** Set `enableResizing: false` on individual column definitions to prevent them from being resized. The select, expand, and actions utility columns disable resizing by default. - ---- - -## Column Helpers & Hooks - -### Column factories - -```tsx -import { createSelectColumn, createExpandColumn, createActionsColumn } from "@/components/data-table"; -import type { ActionDefinition } from "@/components/data-table"; - -const columns = [ - createSelectColumn(), // Checkbox column - createExpandColumn(), // Expand toggle column - { accessorKey: "name", header: "Name" }, - createActionsColumn((row) => [ - { label: "Edit", onSelect: (r) => router.push(`/users/${r.id}/edit`) }, - { label: "Copy ID", onSelect: (r) => navigator.clipboard.writeText(r.id) }, - { type: "separator" }, - { - label: "Delete", - variant: "destructive", - onSelect: async (r, { removeRow }) => { - await api.deleteUser(r.id); - removeRow(r.id); - }, - }, - ]), -]; -``` - -### `createActionsColumn` — Row actions dropdown - -Creates a three-dot (`⋮`) dropdown menu column. Place as the last column. The `getActions` callback receives the row data and returns action definitions. - -**Action item fields:** - -| Field | Type | Default | Description | -| ---------- | -------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ | -| `label` | `string` | — | Menu item text | -| `onSelect` | `(row: TData, actions: DataTableActions) => void` | — | Callback when item is clicked. Second arg provides `reloadPage`, `updateRow`, `removeRow`. | -| `variant` | `"default" \| "destructive"` | `"default"` | Visual style (red for danger) | -| `disabled` | `boolean` | `false` | Disable the item | -| `icon` | `ReactNode` | — | Icon rendered before the label | - -Use `{ type: "separator" }` between items to add a visual divider. - -### Hooks - -```tsx -import { useDataTable, useDataTableState, useDataTableActions } from "@/components/data-table"; - -// TanStack Table instance — access rows, columns, state, handlers -const table = useDataTable(); - -// DataTable state context — loading, error, infinite scroll, feature flags -const { isLoading, hasMore, loadNextPage, error, retry, enableColumnReordering, enableColumnResizing } = - useDataTableState(); - -// Data mutation actions — for optimistic updates from row actions -const { reloadPage, updateRow, removeRow } = useDataTableActions(); -``` - -### `useDataTableActions()` — Row-level data mutations - -Returns typed helpers for optimistic row updates. **Only works in auto mode** (when `onLoadRows` is provided). Requires `getRowId` on ``. - -| Method | Signature | Description | -| ------------ | --------------------------------------------------------- | --------------------------------------------------------------------------- | -| `reloadPage` | `() => void` | Re-fetches the current page from the server with current params | -| `updateRow` | `(rowId: string, updater: (row: TData) => TData) => void` | Optimistically replaces a row in the current data by ID | -| `removeRow` | `(rowId: string) => void` | Optimistically removes a row from the current data and decrements the count | - -**Usage with row actions:** - -The `onSelect` callback in `createActionsColumn` receives the table actions as a second argument, giving direct access to `reloadPage`, `updateRow`, and `removeRow` without any bridge components or refs: - -```tsx -import { createActionsColumn, type ActionDefinition } from "@/components/data-table"; - -const getRowActions = (row: User): ActionDefinition[] => [ - { - label: "Toggle status", - onSelect: async (r, { updateRow }) => { - const updated = await api.toggleUserStatus(r.id); - updateRow(r.id, () => updated); - }, - }, - { type: "separator" }, - { - label: "Delete", - variant: "destructive", - onSelect: async (r, { removeRow }) => { - await api.deleteUser(r.id); - removeRow(r.id); - }, - }, -]; - -const columns = [...baseColumns, createActionsColumn(getRowActions)]; - - u.id}> - - -; -``` - -### Utility functions - -```tsx -import { getSelectedRows, downloadFile, buildRequestSearchParams } from "@/components/data-table"; - -// Extract original data from selected Row objects -const users: User[] = getSelectedRows(table.getFilteredSelectedRowModel().rows); - -// Trigger a browser file download from a URL -await downloadFile("/api/users/export?format=csv", "users.csv"); -// Resolves filename from: explicit argument → Content-Disposition header → "export" - -// Convert DataTableRequestParams to URLSearchParams -const searchParams = buildRequestSearchParams(params); -``` - ---- - -## Response Contract & Query Params - -### `DataTableResponse` — what `onLoadRows` must return - -```typescript -// Success -{ - success: true, - items: T[], - page: number, // 0-based page index - totalPages: number, - totalCount: number, -} - -// Error — table keeps existing data, shows error state, calls onError callback -{ - success: false, - error: { code: string, message: string }, -} -``` - -### `DataTableRequestParams` — what `onLoadRows` receives - -```typescript -{ - page: number, // 0-based page index - pageSize: number, - sorting: SortingState, // [{ id: "columnId", desc: boolean }] - columnFilters: ColumnFiltersState, - globalFilter: string, -} -``` - -### Full query param table - -`buildRequestSearchParams()` converts `DataTableRequestParams` to `URLSearchParams`: - -| Param | Type | Example | Description | -| ----------------- | ------------------- | --------------------------- | ------------------------------------- | -| `page` | `number` | `0` | 0-based page index | -| `pageSize` | `number` | `50` | Items per page | -| `sortBy` | `string` | `name` | Column accessor to sort by | -| `sortDesc` | `"true" \| "false"` | `false` | Sort direction | -| `search` | `string` | `john` | Global search query | -| `{columnId}` | `string` | `role=admin,editor` | Select filter: comma-separated values | -| `{columnId}` | `"true" \| "false"` | `verified=true` | Boolean filter | -| `{columnId}` | `string \| number` | `name=john` | Text/number/date single-value filter | -| `{columnId}_op` | `string` | `name_op=contains` | Filter operator | -| `{columnId}_min` | `number` | `score_min=10` | Number range: minimum | -| `{columnId}_max` | `number` | `score_max=100` | Number range: maximum | -| `{columnId}_from` | `string` | `createdAt_from=2024-01-01` | Date range: start | -| `{columnId}_to` | `string` | `createdAt_to=2024-12-31` | Date range: end | - ---- - -## Building Server Endpoints - -### Data Source Capability Assessment - -Before wiring up a table, assess what the data source can efficiently do: - -**Tier 1 — Full features** (indexed PostgreSQL via resource client): - -- Server-side pagination, sorting, filtering, search, export all efficient -- Enable all table features: pagination, search, filters, column sorting, export -- Requires: proper indexes on sort/filter/search columns - -**Tier 2 — Limited features** (unindexed Postgres, DynamoDB, external APIs with pagination): - -- Some operations are expensive (full table scans for search, sorts on non-key columns) -- Enable: pagination and sorting on indexed/key columns only -- Warn user: "Search and filtering on [column] requires a full table scan. Consider adding an index, or use a simpler table without these features." -- DynamoDB: only sort within partition key, filter on GSI attributes - -**Tier 3 — Client-side only** (Google Sheets, small datasets, APIs without pagination): - -- Fetch all data, use static mode with client-side sorting/filtering -- Use `` instead of `onLoadRows` -- No server-side search/filter overhead -- Warn if dataset > ~1000 rows: "All data is loaded into the browser at once. Performance may degrade with large datasets." - -The AI coder should: - -1. Check resource type and schema/indexes before choosing features -2. Match table capabilities to what the data source can efficiently support -3. Proactively warn user about performance trade-offs when requested features don't match source capabilities -4. Suggest index creation (for managed Postgres) or feature reduction when appropriate - -### Data Access - -Data is accessed through resource clients (`@major-tech/resource-client`), NOT direct database connections. The resource client handles authentication, connection pooling, and multi-tenant isolation. - -For PostgreSQL resources, the resource client executes raw SQL and returns typed results. The patterns below show the SQL to generate — pass them through the resource client's query method. - -### Next.js Route Handler Pattern - -All generated apps are Next.js. Server endpoints go in `app/api/.../route.ts`: - -```typescript -import { NextResponse, type NextRequest } from "next/server"; -import { z } from "zod"; - -// Zod schema for validating query params -const querySchema = z.object({ - page: z.coerce.number().int().min(0).default(0), - pageSize: z.coerce.number().int().min(1).max(200).default(50), - sortBy: z.string().optional(), - sortDesc: z.enum(["true", "false"]).optional(), - search: z.string().optional(), -}); - -// Whitelist of sortable columns — ALWAYS validate against this -const SORTABLE_COLUMNS = ["name", "email", "created_at", "score"] as const; - -export async function GET(request: NextRequest) { - const params = request.nextUrl.searchParams; - - try { - const query = querySchema.parse(Object.fromEntries(params)); - - // 1. Build WHERE clause from filters - const { whereClause, values } = buildWhereClause(params); - - // 2. Validate sort column against whitelist - const sortColumn = SORTABLE_COLUMNS.includes(query.sortBy as any) ? query.sortBy : "created_at"; - const sortDir = query.sortDesc === "true" ? "DESC" : "ASC"; - - // 3. Run count + data queries (can be parallel for better latency) - const offset = query.page * query.pageSize; - - const [countResult, dataResult] = await Promise.all([ - resourceClient.invoke(`SELECT COUNT(*) as count FROM users ${whereClause}`, values, "count-users"), - resourceClient.invoke( - `SELECT * FROM users ${whereClause} ORDER BY ${sortColumn} ${sortDir} LIMIT $${values.length + 1} OFFSET $${values.length + 2}`, - [...values, query.pageSize, offset], - "list-users", - ), - ]); - - if (!countResult.ok || !dataResult.ok) { - throw new Error("Database query failed"); - } - - const totalCount = Number(countResult.result.rows[0].count); - - return NextResponse.json({ - success: true, - items: dataResult.result.rows, - page: query.page, - totalPages: Math.ceil(totalCount / query.pageSize), - totalCount, - }); - } catch (error) { - console.error("Failed to fetch data:", error); - return NextResponse.json( - { success: false, error: { code: "QUERY_FAILED", message: "Failed to fetch data" } }, - { status: 500 }, - ); - } -} -``` - -### Building Postgres WHERE Clauses - -Translate the table's query params into parameterized SQL. Every filter type maps to specific SQL: - -```typescript -interface WhereResult { - whereClause: string; - values: (string | number | boolean)[]; -} - -function buildWhereClause(params: URLSearchParams): WhereResult { - const conditions: string[] = []; - const values: (string | number | boolean)[] = []; - let paramIndex = 1; - - // Global search — OR across multiple columns - const search = params.get("search"); - if (search) { - const q = `%${search}%`; - conditions.push(`(name ILIKE $${paramIndex} OR email ILIKE $${paramIndex + 1})`); - values.push(q, q); - paramIndex += 2; - } - - // Select filter: comma-separated values → IN clause - const role = params.get("role"); - if (role) { - const roles = role.split(","); - const placeholders = roles.map((_, i) => `$${paramIndex + i}`).join(", "); - conditions.push(`role IN (${placeholders})`); - values.push(...roles); - paramIndex += roles.length; - } - - // Text filter with operator - const name = params.get("name"); - if (name) { - const op = params.get("name_op") ?? "contains"; - switch (op) { - case "contains": - conditions.push(`name ILIKE $${paramIndex}`); - values.push(`%${name}%`); - break; - case "eq": - conditions.push(`LOWER(name) = LOWER($${paramIndex})`); - values.push(name); - break; - case "starts_with": - conditions.push(`name ILIKE $${paramIndex}`); - values.push(`${name}%`); - break; - case "ends_with": - conditions.push(`name ILIKE $${paramIndex}`); - values.push(`%${name}`); - break; - case "neq": - conditions.push(`LOWER(name) != LOWER($${paramIndex})`); - values.push(name); - break; - } - paramIndex++; - } - - // Number filter with operator - const scoreOp = params.get("score_op") ?? "eq"; - if (scoreOp === "range") { - const min = params.get("score_min"); - const max = params.get("score_max"); - if (min) { - conditions.push(`score >= $${paramIndex}`); - values.push(Number(min)); - paramIndex++; - } - if (max) { - conditions.push(`score <= $${paramIndex}`); - values.push(Number(max)); - paramIndex++; - } - } else { - const score = params.get("score"); - if (score) { - const num = Number(score); - switch (scoreOp) { - case "eq": - conditions.push(`score = $${paramIndex}`); - break; - case "lt": - conditions.push(`score < $${paramIndex}`); - break; - case "gt": - conditions.push(`score > $${paramIndex}`); - break; - case "neq": - conditions.push(`score != $${paramIndex}`); - break; - } - values.push(num); - paramIndex++; - } - } - - // Date filter with operator - const dateOp = params.get("created_at_op") ?? "eq"; - if (dateOp === "range") { - const from = params.get("created_at_from"); - const to = params.get("created_at_to"); - if (from) { - conditions.push(`created_at >= $${paramIndex}`); - values.push(from); - paramIndex++; - } - if (to) { - conditions.push(`created_at <= $${paramIndex}`); - values.push(to); - paramIndex++; - } - } else { - const dateVal = params.get("created_at"); - if (dateVal) { - switch (dateOp) { - case "eq": - conditions.push(`created_at::date = $${paramIndex}::date`); - break; - case "before": - conditions.push(`created_at < $${paramIndex}`); - break; - case "after": - conditions.push(`created_at > $${paramIndex}`); - break; - case "neq": - conditions.push(`created_at::date != $${paramIndex}::date`); - break; - } - values.push(dateVal); - paramIndex++; - } - } - - // Boolean filter - const verified = params.get("verified"); - if (verified) { - conditions.push(`verified = $${paramIndex}`); - values.push(verified === "true"); - paramIndex++; - } - - const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; - return { whereClause, values }; -} -``` - -### Sorting with Whitelist Validation - -Never interpolate user input directly into ORDER BY. Always validate against a whitelist: - -```typescript -const SORTABLE_COLUMNS: Record = { - name: "name", - email: "email", - created_at: "created_at", - score: "score", -}; - -function buildOrderBy(params: URLSearchParams): string { - const sortBy = params.get("sortBy"); - const sortDesc = params.get("sortDesc") === "true"; - - const column = sortBy && SORTABLE_COLUMNS[sortBy]; - if (!column) { - return "ORDER BY created_at DESC"; // default sort - } - - return `ORDER BY ${column} ${sortDesc ? "DESC" : "ASC"}`; -} -``` - -### Indexing Best Practices - -Create indexes that match your table's common query patterns: - -```sql --- Composite index for common sort + filter combos -CREATE INDEX idx_users_role_created ON users (role, created_at DESC); - --- Partial index for filtered subsets (only rows matching condition are indexed) -CREATE INDEX idx_users_active ON users (created_at DESC) WHERE status = 'active'; - --- Expression index for case-insensitive text search -CREATE INDEX idx_users_name_lower ON users (LOWER(name)); - --- GIN index for trigram similarity (ILIKE '%term%' acceleration) --- Requires: CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE INDEX idx_users_name_trgm ON users USING GIN (name gin_trgm_ops); - --- GIN index for full-text search -CREATE INDEX idx_users_search ON users USING GIN (to_tsvector('english', name || ' ' || email)); -``` - -**When to add indexes:** - -- Add indexes on columns that appear in WHERE, ORDER BY, or JOIN clauses -- Composite indexes: put equality conditions first, range/sort columns last -- Partial indexes: use when queries consistently filter to a subset (e.g. `WHERE deleted_at IS NULL`) -- Skip indexes on low-cardinality columns (e.g. boolean with 50/50 split) — the planner won't use them -- Trade-off: indexes speed up reads but slow down writes. For write-heavy tables with infrequent reads, keep indexing minimal - -### Text Search Strategies - -No Elasticsearch — apps use SQL/NoSQL via resource connectors. Choose the strategy based on dataset size: - -**Small datasets (<10k rows) — plain ILIKE:** - -```sql --- No special setup needed. Fast enough for small tables. -SELECT * FROM users -WHERE name ILIKE '%search%' OR email ILIKE '%search%'; -``` - -**Medium datasets (10k–500k rows) — pg_trgm + GIN index:** - -```sql --- Setup (one-time): enable extension and create index -CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE INDEX idx_users_name_trgm ON users USING GIN (name gin_trgm_ops); - --- Query: same ILIKE, but now index-accelerated -SELECT * FROM users -WHERE name ILIKE '%search%' OR email ILIKE '%search%'; -``` - -**Large datasets (500k+ rows) — tsvector full-text search:** - -```sql --- Setup: add a stored generated column + GIN index -ALTER TABLE users ADD COLUMN search_vector tsvector - GENERATED ALWAYS AS (to_tsvector('english', coalesce(name, '') || ' ' || coalesce(email, ''))) STORED; -CREATE INDEX idx_users_fts ON users USING GIN (search_vector); - --- Query: use plainto_tsquery for user input -SELECT * FROM users -WHERE search_vector @@ plainto_tsquery('english', 'search term') -ORDER BY ts_rank(search_vector, plainto_tsquery('english', 'search term')) DESC; -``` - -**Non-Postgres sources** (Google Sheets, DynamoDB, external APIs): - -- Google Sheets: fetch all rows, filter client-side in static mode -- DynamoDB: use FilterExpression on scan (slow) or design GSIs for searchable attributes -- External APIs: delegate to API's own search params if available, else fetch and filter client-side - -### Postgres Pagination Techniques - -**OFFSET/LIMIT — use with ``:** - -The standard approach. Works with the table's 0-based page index. - -```sql -SELECT * FROM users -WHERE ... -ORDER BY created_at DESC -LIMIT 50 OFFSET 100; -- page 2, pageSize 50 -``` - -Trade-offs: - -- Simple and works with arbitrary page jumps -- Performance degrades on deep pages (Postgres must scan and discard `OFFSET` rows) -- Fine for most tables (users rarely page past page 10–20) -- If performance matters at depth 1000+, consider keyset pagination - -**Keyset/cursor pagination — use with ``:** - -For infinite scroll on very large datasets. Uses the last row's sort key as the cursor. - -```sql --- First page -SELECT * FROM users ORDER BY created_at DESC, id DESC LIMIT 50; - --- Next page (cursor = last row's created_at + id) -SELECT * FROM users -WHERE (created_at, id) < ($1, $2) -ORDER BY created_at DESC, id DESC -LIMIT 50; -``` - -Trade-offs: - -- Consistent performance regardless of depth -- No arbitrary page jumps (forward-only) -- Sort key must be unique or combined with a tiebreaker (e.g. `id`) -- Great for infinite scroll; not suitable for offset pagination - -**Efficient COUNT:** - -For offset pagination, you need `totalCount` for the response: - -```sql --- Exact count — run in parallel with data query -SELECT COUNT(*) FROM users WHERE ...; - --- For very large tables (10M+), consider an approximate count when no filters applied: -SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'users'; --- This is a catalog estimate, updated by ANALYZE. Only use when precision isn't critical. -``` - -Best practice: run the COUNT and data query in parallel using `Promise.all` (shown in the route handler pattern above). This halves the perceived latency. - -### Export Route Handler - -Streaming CSV export endpoint. Applies the same filters/sort as the table but without pagination. - -```typescript -import { NextResponse, type NextRequest } from "next/server"; - -export async function GET(request: NextRequest) { - const params = request.nextUrl.searchParams; - - try { - const { whereClause, values } = buildWhereClause(params); - const orderBy = buildOrderBy(params); - - // No LIMIT/OFFSET — fetch all matching rows - const result = await resourceClient.invoke(`SELECT * FROM users ${whereClause} ${orderBy}`, values, "export-users"); - - if (!result.ok) { - throw new Error("Database query failed"); - } - - // Build CSV - const headers = ["ID", "Name", "Email", "Role", "Created At"]; - const csvRows = [ - headers.join(","), - ...result.result.rows.map((row) => - [row.id, row.name, row.email, row.role, row.created_at] - .map((v) => `"${String(v ?? "").replace(/"/g, '""')}"`) - .join(","), - ), - ]; - const csv = csvRows.join("\n"); - - return new NextResponse(csv, { - headers: { - "Content-Type": "text/csv", - "Content-Disposition": 'attachment; filename="users-export.csv"', - }, - }); - } catch (error) { - console.error("Export failed:", error); - return NextResponse.json( - { success: false, error: { code: "EXPORT_FAILED", message: "Export failed" } }, - { status: 500 }, - ); - } -} -``` - -Frontend usage with ``: - -```tsx -import { DataTableExport, downloadFile } from "@/components/data-table"; - - { - const headers = ["Name", "Email", "Role"]; - return [headers.join(","), ...rows.map((r) => [r.name, r.email, r.role].map((v) => `"${v}"`).join(","))].join("\n"); - }} - filename="users.csv" - onExportAll={async () => { - // Calls the export route with current filters applied - const params = buildRequestSearchParams(currentParams); - await downloadFile(`/api/users/export?${params}`); - }} -/>; -``` - ---- - -## Re-exported Primitives - -For custom table layouts that don't use ``: - -```tsx -import { - Table, - TableHeader, - TableBody, - TableRow, - TableHead, - TableCell, - Button, - Input, - Checkbox, - Select, - SelectValue, - SelectTrigger, - SelectContent, - SelectItem, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, -} from "@/components/data-table"; -``` diff --git a/plugins/shared/skills/http-proxy/SKILL.md b/plugins/shared/skills/http-proxy/SKILL.md deleted file mode 100644 index 774d1ca..0000000 --- a/plugins/shared/skills/http-proxy/SKILL.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -name: using-http-proxy -description: Implements drop-in HTTP proxy access to any connected resource (Stripe, HubSpot, Slack, Gmail, etc.) via the SDK fetch wrapper or generic MCP tools. Use when you need to call a third-party SDK (e.g. Stripe, OpenAI) against a Major resource, or hit an HTTP API endpoint that isn't covered by a typed resource client or specialized MCP tool. ---- - -# Major Platform: HTTP Proxy - -The HTTP proxy lets apps and MCP clients call any HTTP-based resource through a single endpoint — ` /v1/proxy/` with the upstream URL in `X-Major-Target-URL`. The proxy validates the URL against the resource's policy, injects upstream auth, strips reserved headers, and streams the response back. - -**Use this when:** - -- You want to use a third-party SDK (Stripe, OpenAI, Twilio, etc.) that accepts a custom `fetch` and have it route through a connected resource. -- You need to hit an upstream HTTP endpoint that isn't exposed by the typed resource client or a specialized MCP tool. -- You're working with a resource that has no generated client (proxy-only resources). - -**Don't use this when** a specialized MCP tool (e.g. `stripe_list_customers`, `hubspot_get`) or a typed resource client method covers what you need — those are preferred for typed inputs/outputs. - -**Security:** Never set the `Authorization` header — the proxy injects upstream auth. The proxy strips reserved request headers (`Authorization`, `Cookie`, `Host`, `Forwarded`, `X-Forwarded-*`, `X-Real-Ip`) and the `X-Major-*` / `X-Pd-*` namespaces. - ---- - -## SDK: `createProxyFetch` (Next.js) - -`createProxyFetch` returns a `fetch`-compatible function that routes every request through the Major proxy. Drop it into any SDK that accepts a custom `fetch`. - -**Import:** - -```typescript -import { createProxyFetch } from "@major-tech/resource-client/next"; -``` - -**Config:** - -| Field | Type | Required | Notes | -| --------------- | -------------- | -------- | --------------------------------------------------------------------------------------------- | -| `baseUrl` | `string` | yes | Major API base — use `process.env.MAJOR_API_BASE_URL` | -| `resourceId` | `string` | yes | UUID of the resource to proxy through | -| `majorJwtToken` | `string` | yes | App-level JWT — use `process.env.MAJOR_JWT_TOKEN` | -| `fetch` | `typeof fetch` | no | Override runtime fetch (defaults to `globalThis.fetch`) | -| `timeoutMs` | `number` | no | Default `X-Major-Timeout-Ms` (server-clamped to 60_000); only set if caller didn't supply one | - -`MAJOR_API_BASE_URL` and `MAJOR_JWT_TOKEN` are platform-managed env vars — assume they are already set; do not ask the user to provide them. - -`x-major-user-jwt` is auto-forwarded by reading `headers().get("x-major-user-jwt")` from the incoming Next request, so per-user-OAuth resources (Gmail, Calendar, Drive) work transparently. Outside a Next request scope (e.g. background jobs) the lookup is skipped. - -### Drop-in with a third-party SDK - -```typescript -import Stripe from "stripe"; -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const proxyFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.STRIPE_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -const stripe = new Stripe("sk_unused_proxy_injects_real_key", { - httpClient: Stripe.createFetchHttpClient(proxyFetch), -}); - -const customers = await stripe.customers.list({ limit: 10 }); -``` - -The placeholder key is required by the Stripe SDK constructor but is never sent — the proxy strips `Authorization` and injects the real key from the connected resource. - -### Plain fetch - -```typescript -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const proxyFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.HUBSPOT_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -const res = await proxyFetch("https://api.hubapi.com/crm/v3/objects/contacts", { - method: "GET", - headers: { "Content-Type": "application/json" }, -}); - -const data = await res.json(); -``` - -### Framework note - -`createProxyFetch` is exported from `@major-tech/resource-client/next` and uses `next/headers` to forward the user JWT. Use it in Server Components, Route Handlers, or Server Actions only — never in client components. - -### Limits - -- Body and response are clamped to 50 MB by the proxy. -- `X-Major-Timeout-Ms` is clamped to 60_000 (60 s). -- Reserved headers (`Authorization`, `Cookie`, `Host`, `Forwarded`, `X-Forwarded-*`, `X-Real-Ip`, `X-Major-*`, `X-Pd-*`) are silently stripped from requests. - ---- - -## MCP Tools - -The proxy is also exposed as two generic MCP tools that work across every HTTP-based resource the caller has access to. - -- `mcp__resources__http_proxy_invoke` — Make any HTTP method call (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS). Use for writes or when you specifically need a non-GET verb. -- `mcp__resources__http_proxy_get` — Make a GET request. Read-only — safe in restricted contexts. - -**Tool selection:** Prefer a specialized resource tool (e.g. `stripe_list_customers`, `hubspot_get`) when one exists — they have typed args and resource-specific defaults. Fall back to `http_proxy_get` / `http_proxy_invoke` when no specialized tool covers the endpoint. - -### Arguments - -Both tools take: - -| Field | Type | Notes | -| ------------- | ------------------- | ---------------------------------------------------------------------------------- | -| `description` | `string` | Brief label (~5 words) shown to the user in chat | -| `resourceId` | `string` | UUID of the HTTP-based resource — discover with `mcp__resources__list_resources` | -| `url` | `string` | Full upstream URL (e.g. `https://api.hubapi.com/crm/v3/objects/contacts`) | -| `headers` | `Record` | Optional. Do NOT set `Authorization` — the proxy injects it. | -| `timeoutMs` | `number` | Optional. Default 30_000, max 60_000. | - -`http_proxy_invoke` additionally takes: - -| Field | Type | Notes | -| -------- | ------------- | --------------------------------------------------------------------------- | -| `method` | `string` | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS | -| `body` | `RequestBody` | Optional tagged union: `{ type: "json" \| "form" \| "text" \| "bytes", ... }` | - -### Body shape - -| Type | Content-Type | `value` shape | -| -------- | ---------------------------------- | ------------------------------------------------------------------------ | -| `"json"` | `application/json` | Any JSON-serializable value | -| `"form"` | `application/x-www-form-urlencoded`| Flat `Record` — use bracket keys for nesting | -| `"text"` | `text/plain` | `string` | -| `"bytes"`| `contentType` (or `application/octet-stream`) | Set `base64` field, not `value`; optional `contentType` | - -### Discovering proxy-compatible resources - -`mcp__resources__list_resources` returns each resource with a `proxy` field: - -```json -{ - "id": "...", - "subtype": "stripe", - "proxy": { "compatible": true, "baseUrls": ["https://api.stripe.com"] } -} -``` - -Use `proxy.baseUrls` as the allowed host prefixes when constructing the `url` for an invoke call. - ---- - -## Tips - -- **`resourceId` must be static.** Pass the resource UUID as a string literal or simple identifier (e.g. an env var read at module scope). Dynamic expressions won't work — the resource won't be tracked against your app. -- **No `Authorization` from your side.** Setting it does nothing (the proxy strips it) and risks leaking a key into a log. Let the proxy inject upstream auth. -- **Per-user OAuth resources** (Gmail, Calendar, Drive) work automatically inside a Next request scope — the user JWT is auto-forwarded. In background jobs, only the app-level JWT is sent, so per-user calls will fail. diff --git a/plugins/shared/skills/major-cli-prototype/SKILL.md b/plugins/shared/skills/major-cli-prototype/SKILL.md deleted file mode 100644 index 9a9aa82..0000000 --- a/plugins/shared/skills/major-cli-prototype/SKILL.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: major-cli-prototype -description: Develop an existing Major app through the Major CLI, locally or in its mounted workspace. ---- - -Use the shell that executes in the app workspace. Use `--non-interactive` for Major commands and `--json` when consuming results. Never print credentials. Do not use raw API requests to perform app-management steps. - -1. Inspect with `major app info --non-interactive --json`. -2. Use `major vars list --non-interactive --json` only when environment values are needed; treat returned values as secrets. Use a named test key for validation, not customer credentials. -3. Edit using normal file tools. Review changes; commit and push explicitly. Never force-push or switch branches implicitly. -4. Deploy only when the user explicitly asks. Use `major app deploy --non-interactive --no-wait --json`; supply `--slug` on first deploy. Record the returned version ID. -5. Use the emitted status command to inspect that deployment. A started deployment is not yet deployed. Use bounded status checks, never an unbounded polling loop. -6. Read bounded logs with `major app logs --non-interactive --json --limit 20` when needed. -7. If inputs or confirmation are missing, provide the named flags only when the user's intent authorizes them. Never treat `--non-interactive` as `--yes`. -8. After an uncertain deploy response, inspect state before retrying. Never assume a timeout means no mutation happened. diff --git a/plugins/shared/skills/memory/SKILL.md b/plugins/shared/skills/memory/SKILL.md deleted file mode 100644 index b8982c3..0000000 --- a/plugins/shared/skills/memory/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: using-memory -description: Use this skill when you need to recall knowledge about the organization's resources, data patterns, or conventions from previous sessions. Memory is read-only — you can view and search existing memories but cannot create, edit, or delete them. ---- - -# Memory Tools - -Memory tools let you access knowledge recorded in previous coding sessions. Facts stored here are available to all sessions in this organization. - -**Memory is read-only for this agent.** You can view and search existing memory files to inform your work, but you cannot create, edit, or delete them. - -## Tools (on major-platform MCP server) - -| Tool | Purpose | -| ---------------------------------- | -------------------------------- | -| `mcp__major-platform__memory_view` | List files or view file contents | -| `mcp__major-platform__memory_grep` | Search memory files by content | - -## File Structure - -``` -memory/ - organization/ # Org-wide knowledge - api-patterns.md # Common API patterns, conventions - data-model.md # Key data model facts - ... - resources// # Resource-specific knowledge - schema-notes.md # Schema details, column meanings - query-patterns.md # Common query patterns - ... -``` - -## When to Use - -Check memory at the start of a session or when working with resources to see if previous sessions recorded useful context (schema details, API patterns, conventions). - -## Examples - -**View all memory files:** - -``` -memory_view(path: "memory/") -``` - -**View resource-specific files:** - -``` -memory_view(path: "memory/resources/abc-123-def/") -``` - -**Search for relevant knowledge:** - -``` -memory_grep(pattern: "users table", file_glob: "memory/organization/**/*.md") -``` diff --git a/plugins/shared/skills/resources_ai-proxy/SKILL.md b/plugins/shared/skills/resources_ai-proxy/SKILL.md deleted file mode 100644 index e867ac1..0000000 --- a/plugins/shared/skills/resources_ai-proxy/SKILL.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -name: using-ai-proxy -description: Use when the user asks to add AI features, LLM calls, or chat functionality to their app. Covers Major's built-in AI proxy for Anthropic, OpenAI, and Gemini APIs. ---- - -## Major AI Proxy - -Major provides a built-in AI proxy that lets apps call Anthropic, OpenAI, and Gemini APIs without configuring API keys. Usage is billed at cost against the user's credits. - -## Workflow - -Run these commands in the app workspace (through `mcp__plugin_major-build_major__sandbox_bash` when working in a hosted sandbox). - -1. Run `major app ai-proxy status` to see if the proxy is enabled for this app -2. If enabled: use it directly with the env vars below -3. If not enabled: ask the user if they want to enable it (recommended) or use their own API keys -4. If user wants to enable it: run `major app ai-proxy enable` (starts with a $10/month spending limit) - -## Environment Variables - -These env vars are already available in the app's runtime environment: - -- `MAJOR_AI_PROXY_URL` — Base URL for the AI proxy (set in `.env`) -- `MAJOR_JWT_TOKEN` — Authentication token (set as pod env var). **You MUST pass this as the `apiKey` when initializing the SDK client.** The proxy authenticates every request using this token — requests without it will be rejected. - -They might not be available in your Bash, that's normal. - -## Code Examples - -### Anthropic - -```typescript -import Anthropic from "@anthropic-ai/sdk"; - -const client = new Anthropic({ - baseURL: process.env.MAJOR_AI_PROXY_URL + "/anthropic", - apiKey: process.env.MAJOR_JWT_TOKEN, -}); - -const message = await client.messages.create({ - model: "claude-sonnet-4-6", - max_tokens: 1024, - messages: [{ role: "user", content: "Hello!" }], -}); -``` - -### OpenAI — Chat - -```typescript -import OpenAI from "openai"; - -const client = new OpenAI({ - baseURL: process.env.MAJOR_AI_PROXY_URL + "/openai", - apiKey: process.env.MAJOR_JWT_TOKEN, -}); - -const completion = await client.chat.completions.create({ - model: "gpt-4.1", - messages: [{ role: "user", content: "Hello!" }], -}); -``` - -### OpenAI — Text-to-Speech - -```typescript -const speech = await client.audio.speech.create({ - model: "tts-1", - voice: "alloy", - input: "Hello, world!", -}); -``` - -### OpenAI — Speech-to-Text - -```typescript -const transcription = await client.audio.transcriptions.create({ - model: "whisper-1", - file: audioFile, -}); -``` - -### OpenAI — Image Generation - -Three stateless endpoints. None persist images server-side — input image bytes only live in the request body. - -```typescript -// Text → image -const image = await client.images.generate({ - model: "gpt-image-1", - prompt: "A white siamese cat", - n: 1, - size: "1024x1024", -}); - -// Image + prompt → edited image -const edited = await client.images.edit({ - model: "gpt-image-1", - image: fs.createReadStream("input.png"), - prompt: "Add a top hat", -}); - -// Image → variations (DALL-E 2 only) -const variations = await client.images.createVariation({ - image: fs.createReadStream("input.png"), - n: 2, -}); -``` - -### Gemini - -Use the official `@google/genai` SDK. Point its `baseUrl` at the proxy's `/genai` prefix — the SDK appends `/v1beta/models/...` itself. - -```typescript -import { GoogleGenAI } from "@google/genai"; - -const ai = new GoogleGenAI({ - apiKey: process.env.MAJOR_JWT_TOKEN, - httpOptions: { - baseUrl: process.env.MAJOR_AI_PROXY_URL + "/genai", - }, -}); - -const response = await ai.models.generateContent({ - model: "gemini-2.5-pro", - contents: "Hello!", -}); -``` - -Streaming uses `ai.models.generateContentStream(...)` and counting tokens uses `ai.models.countTokens(...)` with the same client. - -### Gemini — Image Generation - -Two paths: an image-capable model via `generateContent` (output comes back as `inlineData` parts), or the dedicated `generateImages` endpoint for Imagen. - -```typescript -// Gemini image models via generateContent -const response = await ai.models.generateContent({ - model: "gemini-2.5-flash-image", - contents: "A picture of a banana dish in a fancy restaurant", -}); - -for (const part of response.candidates[0].content.parts) { - if (part.inlineData) { - const dataUrl = `data:image/png;base64,${part.inlineData.data}`; - } -} - -// Imagen via generateImages -const imagen = await ai.models.generateImages({ - model: "imagen-3.0-generate-002", - prompt: "Robot holding a red skateboard", - config: { numberOfImages: 1 }, -}); - -const imageBytes = imagen.generatedImages?.[0]?.image?.imageBytes; -``` - -## Available Models - -**Anthropic:** - -- `claude-opus-4-7` (flagship) -- `claude-sonnet-4-6` (mid-tier) -- `claude-haiku-4-5-20251001` (fast/cheap) - -**OpenAI:** - -- `gpt-5.4` (flagship) -- `gpt-4.1` (mid-tier, 1M context) -- `gpt-4.1-mini` (good value, 1M context) -- `gpt-4.1-nano` (cheapest) -- `o3` (reasoning) -- `o4-mini` (fast reasoning) - -**Gemini:** - -- `gemini-2.5-pro` (flagship, reasoning) -- `gemini-2.5-flash` (fast, mid-tier) -- `gemini-2.5-flash-lite` (cheapest) - -**Image generation:** - -- `gpt-image-1`, `gpt-image-2` (OpenAI, via `images.generate` / `images.edit`) -- `dall-e-2` (OpenAI, the only model that supports `images.createVariation`) -- `gemini-2.5-flash-image`, `gemini-3-pro-image-preview` (Gemini, via `generateContent`) -- `imagen-3.0-generate-002` (Gemini, via `generateImages`) - -## Rules - -- Always use the native provider SDK (`@anthropic-ai/sdk`, `openai`, or `@google/genai`) — never a unified SDK -- Set the base URL to `MAJOR_AI_PROXY_URL + "/"` — `/anthropic`, `/openai`, or `/genai`. For Anthropic and OpenAI, this is the SDK's `baseURL`; for Gemini, it's `httpOptions.baseUrl`. -- **Set `apiKey` to `process.env.MAJOR_JWT_TOKEN`** — this is required for authentication. Without it, all requests will be rejected. -- Never hardcode API keys or proxy URLs -- Only use models from the allowlist above - -## Available Endpoints - -Only these endpoints are available through the proxy: - -- **Anthropic:** `/v1/messages` (chat) -- **OpenAI:** `/v1/chat/completions` (chat), `/v1/responses` (responses API), `/v1/audio/speech` (text-to-speech), `/v1/audio/transcriptions` (speech-to-text), `/v1/images/generations`, `/v1/images/edits`, `/v1/images/variations` -- **Gemini:** `/v1beta/models/{model}:generateContent`, `/v1beta/models/{model}:streamGenerateContent`, `/v1beta/models/{model}:countTokens`, `/v1beta/models/{model}:generateImages` - -All image endpoints are stateless — input image bytes only live in the request body for that one call. There's no Files API, no Assistants/threads, and no server-side persistence. - -## Limitations - -- No embeddings or video generation -- Anthropic, OpenAI, and Gemini only -- Gemini: chat, streaming, token counting, and image generation only — embeddings, files, video, cached contents, tuning, and the Live API are not supported -- Monthly spending limits apply — requests are rejected if limit exceeded or wallet empty -- Specific models from the allowlist only diff --git a/plugins/shared/skills/resources_bigquery/SKILL.md b/plugins/shared/skills/resources_bigquery/SKILL.md deleted file mode 100644 index ddab831..0000000 --- a/plugins/shared/skills/resources_bigquery/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: using-bigquery-connector -description: Implements BigQuery dataset exploration, SQL queries, and table operations using generated clients and MCP tools. Use when doing ANYTHING that touches BigQuery or BQ in any way, load this skill. ---- - -# Major Platform Resource: BigQuery - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__bigquery_list_datasets` — List all datasets in the project. Args: `resourceId` -- `mcp__resources__bigquery_list_tables` — List tables in a dataset. Args: `resourceId`, `datasetId` -- `mcp__resources__bigquery_describe_table` — Get schema and metadata for a table. Args: `resourceId`, `datasetId`, `tableId` -- `mcp__resources__bigquery_query` — Execute read-only SQL (SELECT only). Args: `resourceId`, `statement` - -## TypeScript Client - -```typescript -import { bqClient } from "./clients"; - -// query(sql, params?, invocationKey, options?) -const result = await bqClient.query( - "SELECT * FROM `project.dataset.table` WHERE created > @cutoff", - { cutoff: "2024-01-01" }, - "recent-records", - { maxResults: 1000 }, -); - -// Other methods: listDatasets, listTables, getTable, insertRows, createTable -``` - -## Tips - -- **Be cost-aware** — BigQuery charges per bytes scanned. Use `SELECT specific_columns` instead of `SELECT *`. Use `LIMIT` during exploration. -- Use `list_datasets` → `list_tables` → `describe_table` to understand data structure before querying -- Use `maxResults` option for pagination of large result sets -- Named parameters use `@param` syntax in queries - -**Docs**: [BigQuery Documentation](https://cloud.google.com/bigquery/docs) diff --git a/plugins/shared/skills/resources_clerk/SKILL.md b/plugins/shared/skills/resources_clerk/SKILL.md deleted file mode 100644 index 9fbf43b..0000000 --- a/plugins/shared/skills/resources_clerk/SKILL.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: using-clerk-connector -description: Implements Clerk Backend API requests with automatic Bearer Token auth using generated clients and MCP tools. Use when doing ANYTHING that touches a Clerk resource in any way, load this skill. ---- - -# Major Platform Resource: Clerk Backend API - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Clerk:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Clerk API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-clerk-users"`, never dynamic values. - ---- - -## MCP Tools - -- `mcp__resources__clerk_get` — Make a GET request to any Clerk Backend API endpoint. Args: `resourceId`, `path`, `query?` -- `mcp__resources__clerk_list_users` — List users with optional filtering. Args: `resourceId`, `emailAddress?`, `limit?`, `offset?` -- `mcp__resources__clerk_get_user` — Get a single user by ID. Args: `resourceId`, `userId` -- `mcp__resources__clerk_list_organizations` — List organizations. Args: `resourceId`, `limit?`, `offset?` -- `mcp__resources__clerk_invoke` — Make any HTTP request to the Clerk API (including writes). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { clerkClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -const result = await clerkClient.invoke("GET", "/v1/users", "list-users", { - query: { limit: "10", offset: "0" }, -}); -if (result.ok) { - const response = result.result; - // response: { kind: "api", status: number, body: { kind: "json"|"text"|"binary", value: ... } } -} - -// POST example - create an invitation -await clerkClient.invoke("POST", "/v1/invitations", "create-invitation", { - body: { type: "json", value: { email_address: "user@example.com" } }, -}); -``` - -## Example: Search for Users by Email - -**Using MCP tools (no code needed):** - -Call `mcp__resources__clerk_list_users` with the `emailAddress` filter: - -``` -mcp__resources__clerk_list_users({ - resourceId: "", - emailAddress: "jane@example.com", - limit: "10" -}) -``` - -**Using the TypeScript client in a Next.js Server Action:** - -```typescript -"use server"; - -import { clerkClient } from "./clients"; - -interface ClerkUser { - id: string; - first_name: string | null; - last_name: string | null; - email_addresses: { email_address: string }[]; - created_at: number; -} - -export async function searchUsersByEmail(email: string) { - const result = await clerkClient.invoke("GET", "/v1/users", "search-users-by-email", { - query: { email_address: email, limit: "20" }, - }); - - if (!result.ok) { - throw new Error(result.error.message); - } - - const users = result.result.body.value as ClerkUser[]; - - return users.map((u) => ({ - id: u.id, - name: [u.first_name, u.last_name].filter(Boolean).join(" "), - email: u.email_addresses[0]?.email_address ?? "", - createdAt: new Date(u.created_at), - })); -} -``` - -## Tips - -- **Base URL is always `https://api.clerk.com`** — paths should include the version prefix (e.g. `/v1/users`) -- **Auth is automatic** — the Secret Key is sent as a Bearer token on every request -- **Clerk API version**: The connector targets the Clerk Backend API. Refer to [Clerk Backend API docs](https://clerk.com/docs/reference/backend-api) for endpoint details -- **Common endpoints**: `/v1/users`, `/v1/organizations`, `/v1/invitations`, `/v1/sessions`, `/v1/clients` -- **Pagination**: Most list endpoints support `limit` and `offset` query parameters diff --git a/plugins/shared/skills/resources_clickhouse/SKILL.md b/plugins/shared/skills/resources_clickhouse/SKILL.md deleted file mode 100644 index a9e695b..0000000 --- a/plugins/shared/skills/resources_clickhouse/SKILL.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: using-clickhouse-connector -description: Implements ClickHouse database connections, SQL queries, and data operations using generated clients and MCP tools. Use when doing ANYTHING that touches ClickHouse in any way, load this skill. ---- - -# Major Platform Resource: ClickHouse - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__clickhouse_query` — Execute a read-only ClickHouse query. Supports SELECT and introspection statements like `SHOW TABLES`, `DESCRIBE table_name`, `SHOW DATABASES`, `SHOW CREATE TABLE table_name`, `EXISTS TABLE table_name`, and `EXPLAIN`. **Does not require user approval — prefer this tool for all read-only operations.** Args: `resourceId`, `statement`, `params?`, `description`, `timeoutMs?` -- `mcp__resources__clickhouse_invoke` — Execute any SQL statement including write operations (INSERT, ALTER, CREATE, DROP, OPTIMIZE, TRUNCATE, etc.). Returns rows and rowsAffected. **Requires user approval — only use when you need to write data.** Args: `resourceId`, `sql`, `params?`, `description`, `timeoutMs?` - -**IMPORTANT: Always use `clickhouse_query` for read-only operations.** It does not require user approval, making the workflow faster and smoother. Only use `clickhouse_invoke` when you actually need to perform writes (INSERT, ALTER TABLE, etc.). Never use `clickhouse_invoke` for SELECT queries or schema exploration. - -## TypeScript Client - -```typescript -import { myClickhouseClient } from "./clients"; - -// invoke(sql, params?, invocationKey, timeoutMs?) -// Uses positional ? placeholders -const result = await myClickhouseClient.invoke<{ id: number; name: string }>( - "SELECT * FROM users WHERE id = ?", - [userId], - "fetch-user", -); -if (result.ok) { - console.log(result.result.rows); -} -``` - -## Tips - -- **Use `clickhouse_query` exclusively for read-only tasks. Never use `clickhouse_invoke` for read-only.** -- Uses **positional `?` placeholders** — not `$1, $2` like PostgreSQL -- Default native TCP port is **9000** (secure: 9440). HTTP port is 8123 (secure: 8443) -- Use `clickhouse_query` with `SHOW DATABASES`, `SHOW TABLES`, `DESCRIBE table_name`, `SHOW CREATE TABLE table_name`, `EXISTS TABLE table_name` to explore database structure -- ClickHouse is a **columnar OLAP database** — optimized for aggregation queries over large datasets, not for row-level transactions. No UPDATE/DELETE in the traditional SQL sense (use `ALTER TABLE ... UPDATE/DELETE` for mutations) -- **No transactions** — ClickHouse does not support BEGIN/COMMIT/ROLLBACK -- INSERT uses standard SQL: `INSERT INTO table (col1, col2) VALUES (?, ?)` -- String values use single quotes. Identifiers use double quotes or backticks -- ClickHouse-specific types: `DateTime64`, `Decimal`, `UUID`, `Array(T)`, `Tuple(T1, T2)`, `Map(K, V)`, `Nullable(T)`, `LowCardinality(T)`, `Enum8/Enum16` -- `FINAL` keyword forces merging of data parts: `SELECT * FROM table FINAL` -- Use `FORMAT JSON` or `FORMAT JSONEachRow` when you need specific output formats, but the connector handles serialization automatically -- `LIMIT n` for pagination; `OFFSET` is supported but avoid large offsets (use `WHERE` clauses instead) -- Table engines matter: `MergeTree` family for analytics, `ReplicatedMergeTree` for clusters - -**Docs**: [ClickHouse SQL Reference](https://clickhouse.com/docs/en/sql-reference) diff --git a/plugins/shared/skills/resources_cosmosdb/SKILL.md b/plugins/shared/skills/resources_cosmosdb/SKILL.md deleted file mode 100644 index 2947465..0000000 --- a/plugins/shared/skills/resources_cosmosdb/SKILL.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -name: using-cosmosdb-connector -description: Implements Azure CosmosDB container queries, CRUD, and patch operations using generated clients and MCP tools. Use when doing ANYTHING that touches CosmosDB or Cosmos DB in any way, load this skill. ---- - -# Major Platform Resource: Azure CosmosDB - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__cosmosdb_list_containers` — List containers with partition key info. Args: `resourceId` -- `mcp__resources__cosmosdb_describe_container` — Get partition key, indexing policy, unique keys. Args: `resourceId`, `container` -- `mcp__resources__cosmosdb_get_container_stats` — Get document count and storage size. Args: `resourceId`, `container` -- `mcp__resources__cosmosdb_query` — Execute a SQL query against a container. Args: `resourceId`, `container`, `query`, `parameters?`, `maxItemCount?` - -## TypeScript Client - -```typescript -import { cosmosClient } from "./clients"; - -// query(container, query, invocationKey, options?) -const result = await cosmosClient.query( - "users", - "SELECT * FROM c WHERE c.status = @status", - "fetch-active-users", - { - parameters: [{ name: "@status", value: "active" }], - partitionKey: "tenant-acme", - maxItemCount: 50, - }, -); -if (result.ok) { - console.log(result.result.documents); - // Handle pagination: result.result.continuationToken -} - -// Other methods: read, create, upsert, replace, delete, patch -await cosmosClient.patch( - "users", - "user-456", - "tenant-acme", - [ - { op: "set", path: "/name", value: "Jane" }, - { op: "incr", path: "/loginCount", value: 1 }, - ], - "update-user", -); -``` - -## Tips - -- **Partition key is required** for point operations (`read`, `replace`, `delete`, `patch`). Omit for cross-partition queries. -- Use `patch()` for partial updates — more efficient than full `replace()` -- Query parameters use `@param` syntax: `[{ name: "@status", value: "active" }]` -- Use `continuationToken` from query results for pagination through large result sets - -**Docs**: [CosmosDB Documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/) diff --git a/plugins/shared/skills/resources_custom-api/SKILL.md b/plugins/shared/skills/resources_custom-api/SKILL.md deleted file mode 100644 index 6059ce8..0000000 --- a/plugins/shared/skills/resources_custom-api/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: using-custom-api-connector -description: Implements custom REST API HTTP requests with automatic auth header injection using generated clients and MCP tools. Use when doing ANYTHING that touches a custom API resource in any way, load this skill. ---- - -# Major Platform Resource: Custom REST API - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Custom API:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Custom API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__custom_get` — Make a GET request to the configured API endpoint. Args: `resourceId`, `path`, `queryParams?` - -## TypeScript Client - -```typescript -import { apiClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -const result = await apiClient.invoke("GET", "/users", "list-users", { query: { page: "1", limit: "20" } }); -if (result.ok) { - const response = result.result; - // response: { kind: "api", status: number, body: { kind: "json"|"text"|"binary", value: ... } } -} - -// POST with body -await apiClient.invoke("POST", "/users", "create-user", { - body: { type: "json", value: { name: "Jane", email: "jane@example.com" } }, -}); -``` - -## Tips - -- **Paths are relative to the resource's configured base URL** -- **Auth headers are automatically injected** — the resource configuration includes secret headers (e.g., Authorization) that you don't need to set manually -- Supports all HTTP methods: GET, POST, PUT, PATCH, DELETE -- Custom headers can be added via the `headers` option in the TypeScript client - -**Docs**: Refer to the specific API's documentation for endpoint details. diff --git a/plugins/shared/skills/resources_dynamodb/SKILL.md b/plugins/shared/skills/resources_dynamodb/SKILL.md deleted file mode 100644 index 46c6797..0000000 --- a/plugins/shared/skills/resources_dynamodb/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: using-dynamodb-connector -description: Implements DynamoDB queries, scans, and CRUD operations using generated clients and MCP tools. Use when doing ANYTHING that touches DynamoDB, DDB, or Dynamo in any way, load this skill. ---- - -# Major Platform Resource: DynamoDB - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__dynamodb_list_tables` — List all accessible tables. Args: `resourceId`, `limit?` -- `mcp__resources__dynamodb_describe_table` — Get table schema, keys, indexes, throughput. Args: `resourceId`, `tableName` -- `mcp__resources__dynamodb_scan` — Read-only scan with optional filter. Args: `resourceId`, `tableName`, `limit?`, `filterExpression?`, `projectionExpression?` - -## TypeScript Client - -```typescript -import { myDynamoClient } from "./clients"; - -// invoke(command, params, invocationKey) -const result = await myDynamoClient.invoke( - "Query", - { - TableName: "orders", - KeyConditionExpression: "user_id = :uid", - ExpressionAttributeValues: { ":uid": { S: userId } }, - Limit: 20, - ScanIndexForward: false, - }, - "fetch-user-orders", -); - -if (result.ok) { - console.log(result.result.data.Items); -} -``` - -## Tips - -- **Prefer Query over Scan** — Scan reads every item in the table and is expensive at scale -- Use `describe_table` to understand key schema and indexes before writing queries -- DynamoDB attribute values use marshall format (`{ S: "string" }`, `{ N: "123" }`, `{ BOOL: true }`) -- TypeScript client supports all DynamoDB commands: `GetItem`, `PutItem`, `Query`, `Scan`, `UpdateItem`, `DeleteItem`, `BatchGetItem`, `BatchWriteItem` - -**Docs**: [DynamoDB Developer Guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/) diff --git a/plugins/shared/skills/resources_fireflies/SKILL.md b/plugins/shared/skills/resources_fireflies/SKILL.md deleted file mode 100644 index 3e08ec9..0000000 --- a/plugins/shared/skills/resources_fireflies/SKILL.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: using-fireflies-connector -description: Implements Fireflies AI meeting transcription API access for transcripts, users, summaries, and audio upload using generated clients and MCP tools. Use when doing ANYTHING that touches Fireflies in any way, load this skill. ---- - -# Major Platform Resource: Fireflies - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Fireflies:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Fireflies API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `response.ok` before accessing `response.data`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-transcripts"`, never dynamic values like `` `${date}-transcripts` ``. - ---- - -## MCP Tools - -- `mcp__resources__fireflies_query` — Execute any GraphQL query against the Fireflies API. Supports introspection queries. Args: `resourceId`, `query`, `variables?` -- `mcp__resources__fireflies_mutate` — Execute any GraphQL mutation against the Fireflies API. Args: `resourceId`, `query`, `variables?` -- `mcp__resources__fireflies_list_transcripts` — List transcripts with optional filters. Args: `resourceId`, `keyword?`, `fromDate?`, `toDate?`, `limit?`, `skip?`, `hostEmail?`, `mine?` -- `mcp__resources__fireflies_get_transcript` — Get transcript by ID with summary, speakers, sentences, analytics. Args: `resourceId`, `transcriptId` -- `mcp__resources__fireflies_list_users` — List all team users. Args: `resourceId` -- `mcp__resources__fireflies_get_user` — Get a single user by ID. Args: `resourceId`, `userId` -- `mcp__resources__fireflies_upload_audio` — Upload audio from a public URL for transcription. Args: `resourceId`, `url`, `title?`, `attendees?` - -## TypeScript Client - -The client exposes `query()` and `mutate()` methods. The generic `T` types the parsed GraphQL data payload directly — no need to dig through result.body.kind / result.body.value / data. - -### Reading data - -```typescript -import { firefliesClient } from "./clients"; - -// List recent transcripts -const response = await firefliesClient.query<{ - transcripts: Array<{ id: string; title: string; date: number; duration: number }>; -}>( - `query { transcripts(limit: 10) { id title date duration } }`, - "list-transcripts" -); - -if (response.ok) { - for (const t of response.data.transcripts) { - console.log(t.id, t.title); - } -} - -// Get a transcript with summary -const transcript = await firefliesClient.query<{ - transcript: { - id: string; title: string; duration: number; - summary: { overview: string; action_items: string; short_summary: string }; - speakers: Array<{ id: string; name: string }>; - }; -}>( - `query($id: String!) { - transcript(id: $id) { - id title duration - summary { overview action_items short_summary } - speakers { id name } - } - }`, - "get-transcript", - { variables: { id: "transcript-id" } } -); - -// List users -const users = await firefliesClient.query<{ - users: Array<{ user_id: string; email: string; name: string }>; -}>( - `query { users { user_id email name } }`, - "list-users" -); -``` - -### Writing data - -```typescript -// Upload audio via mutation -const upload = await firefliesClient.mutate<{ - uploadAudio: { success: boolean; title: string; message: string }; -}>( - `mutation($input: AudioUploadInput) { - uploadAudio(input: $input) { success title message } - }`, - "upload-audio", - { variables: { input: { url: "https://...", title: "My Meeting" } } } -); - -if (upload.ok) { - console.log("Upload:", upload.data.uploadAudio.message); -} -``` - -### Search with variables - -```typescript -const filtered = await firefliesClient.query<{ - transcripts: Array<{ id: string; title: string }>; -}>( - `query($keyword: String, $limit: Int) { - transcripts(keyword: $keyword, limit: $limit) { id title } - }`, - "search-transcripts", - { variables: { keyword: "sales call", limit: 5 } } -); -``` - -## Tips - -- **Rate limits**: Free/Pro = 50 req/day, Business/Enterprise = 60 req/min -- Only request fields you need — GraphQL precision reduces response size -- Transcripts `limit` max is 50 per query; use `skip` for pagination -- Audio upload only works with publicly accessible HTTPS URLs -- `audio_url` and `video_url` expire after 24 hours — re-query if needed -- Meeting `date` field is in epoch milliseconds (UTC) -- Common queries: `transcripts`, `transcript` (by ID), `users`, `user` (by ID) -- Common mutations: `uploadAudio`, `deleteTranscript`, `updateMeetingTitle` -- Introspection is supported — use `{ __schema { ... } }` or `{ __type(name: "Transcript") { ... } }` via the `query` tool to discover available fields - -**Docs**: [Fireflies API Reference](https://docs.fireflies.ai) diff --git a/plugins/shared/skills/resources_github/SKILL.md b/plugins/shared/skills/resources_github/SKILL.md deleted file mode 100644 index 7d9758d..0000000 --- a/plugins/shared/skills/resources_github/SKILL.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -name: using-github-connector -description: Implements GitHub repository, issue, pull request, release, content, branch, and authenticated git operations using the GitHub connector, mounted MCP tools, generated clients, and HTTP proxy. Use when doing ANYTHING that touches GitHub in any way, including cloning a private repository. ---- - -# Major Platform Resource: GitHub - -## Setting Up a GitHub Connector - -GitHub uses a GitHub App installation. When the user asks to connect GitHub: - -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "github"`. -2. Ask the user to finish the GitHub installation flow and select the repositories the app may access. -3. After setup completes, call `mcp__resources__list_resources` and use the connected GitHub resource's `resourceId` and mounted MCP slug. - ---- - -## Common: Interacting with Resources - -**Security:** Never connect to GitHub with credentials placed in source code. Use the connector's MCP tools, generated client, or HTTP proxy. Only request a raw token for git operations such as clone, fetch, or push. GitHub installation tokens are short-lived secrets: never print, log, commit, or persist them. - -**Description field:** Include a short `description` (~5 words) in resource MCP calls that accept it, such as `"Get token to clone repo"`. - -**Four ways to interact with GitHub:** - -1. **Mounted GitHub MCP tools** (direct, preferred): The connected GitHub MCP server exposes tools as `mcp____`. Use `mcp__resources__list_resources` to discover the resource and its slug. The hosted tool catalog covers repositories, files, issues, pull requests, branches, commits, and releases. -2. **Git token tool** (git CLI only): Call `mcp__resources__github_get_git_token` with the GitHub `resourceId`. Optionally downscope it to repository names and permissions. -3. **Generated TypeScript client** (app code): Call `mcp__resource-tools__add-resource-client` with the `resourceId`. The generated client is created in `/clients/` (Next.js) or `/src/clients/` (Vite). -4. **HTTP proxy** (Next.js app code or direct MCP calls): Use `createProxyFetch` from `@major-tech/resource-client/next`, or `mcp__resources__http_proxy_get` / `mcp__resources__http_proxy_invoke`, for GitHub REST or GraphQL endpoints not covered by a mounted MCP tool. See [using-http-proxy](../http-proxy/SKILL.md). - -**Do not guess tool names or argument shapes.** Mounted tools come from GitHub's hosted MCP server and may change. Inspect the tools available under the connector's actual slug before calling them. After generating a TypeScript client, read its source to verify exact methods and signatures. - -**Prefer least privilege:** For clone/fetch, request only the target repository with `permissions: { "contents": "read" }`. For push, use `contents: "write"` only when required. - -**Framework note:** In Next.js, generated resource clients and `createProxyFetch` must be used in server-side code only. Never expose a GitHub token or Major JWT to a Client Component or browser. - -**Error handling:** Always check `result.ok` before accessing `result.result` from a generated client. - -**Invocation keys must be static strings** such as `"clone-project-repo"`, never dynamic values such as `` `${owner}-${repo}` ``. - ---- - -## MCP Tools - -### Mounted GitHub tools - -Mounted tools use `mcp____`. Discover the exact catalog and schemas from the connected server. Typical operations include: - -- Reading or updating repository contents -- Listing, creating, and updating issues -- Listing, creating, reviewing, and merging pull requests -- Creating branches and inspecting commits -- Listing and managing releases - -Prefer these tools over minting a raw token when the requested operation is available through MCP. - -### Git token tool - -- `mcp__resources__github_get_git_token` — Mint a short-lived GitHub App installation token for git clone/fetch/push. - -Arguments: - -- `description`: brief user-visible operation label -- `resourceId`: UUID of the connected GitHub resource -- `repositories?`: repository **names** to scope the token to, such as `["my-repo"]` -- `permissions?`: permission subset, such as `{ "contents": "read" }` - -The result includes `token`, `expiresIn`, and may include `gitHost`. Use `gitHost` when present instead of assuming `github.com`, because the connector may target GitHub Enterprise Cloud. - ---- - -## Clone a Repository with a GitHub Token - -**The correct clone flow is to mint a GitHub token first.** Do not try an anonymous clone for a private repository, do not use SSH credentials, and do not put the token directly in the clone URL because Git saves that URL in `.git/config`. - -1. Find the GitHub resource with `mcp__resources__list_resources`. -2. Call `mcp__resources__github_get_git_token`: - -```json -{ - "description": "Get token to clone repo", - "resourceId": "", - "repositories": ["example-repo"], - "permissions": { "contents": "read" } -} -``` - -3. Put the returned token in a temporary environment variable and use a temporary `GIT_ASKPASS` helper. Use the returned `gitHost`, or `github.com` when it is absent: - -```bash -export GITHUB_TOKEN='' -export GITHUB_HOST='' - -askpass="$(mktemp)" -chmod 700 "$askpass" -cat >"$askpass" <<'EOF' -#!/bin/sh -case "$1" in - *Username*) printf '%s\n' 'x-access-token' ;; - *Password*) printf '%s\n' "$GITHUB_TOKEN" ;; -esac -EOF - -GIT_ASKPASS="$askpass" GIT_TERMINAL_PROMPT=0 \ - git clone "https://${GITHUB_HOST}/example-org/example-repo.git" - -rm -f "$askpass" -unset GITHUB_TOKEN GITHUB_HOST -``` - -Use a shell cleanup trap when scripting so the helper and token are removed on failure too. Never echo the token, include it in command output, or construct a remote like `https://x-access-token:@github.com/...`; that form can leak through logs, process arguments, shell history, and `.git/config`. - -If clone returns `Repository not found`, verify that the installed GitHub App can access the repository and that `repositories` contains the repository name (not `owner/name`). Mint a fresh token if it expired. - ---- - -## TypeScript Client - -```typescript -import { githubClient } from "./clients"; - -const result = await githubClient.getGitToken("clone-project-repo", { - repositories: ["example-repo"], - permissions: { contents: "read" }, -}); - -if (!result.ok) { - throw new Error(result.error.message); -} - -// Keep this server-side and in memory only. Do not return or log the token. -const { token, expiresIn } = result.result; -``` - -Use the raw token only when launching a server-side git operation. For normal GitHub API work, prefer mounted MCP tools or the HTTP proxy so authentication remains injected server-side. - ---- - -## HTTP Proxy - -GitHub REST uses `https://api.github.com`; GraphQL uses `https://api.github.com/graphql`. The proxy injects the installation token, so **do not set `Authorization` yourself**. - -```typescript -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const githubFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.GITHUB_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -const response = await githubFetch("https://api.github.com/repos/example-org/example-repo/issues", { - headers: { - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, -}); -``` - -For an enterprise connector, use the base URLs advertised by `mcp__resources__list_resources`; do not hard-code `api.github.com`. - -## Tips - -- GitHub App access is limited to repositories selected during installation and permissions granted to the app. -- Installation tokens generally expire in about one hour; rely on `expiresIn` and mint a new token rather than reusing an expired one. -- A repository selection error is not fixed by requesting broader token permissions; the user must grant the GitHub App access to that repository. -- Use `contents: "read"` for clone/fetch and `contents: "write"` only for push. -- Use mounted MCP tools for API operations so raw credentials stay out of model-authored application code. - -**Docs:** [GitHub MCP server](https://github.com/github/github-mcp-server) · [GitHub REST API](https://docs.github.com/en/rest) · [GitHub App installation tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app) diff --git a/plugins/shared/skills/resources_gmail/SKILL.md b/plugins/shared/skills/resources_gmail/SKILL.md deleted file mode 100644 index 6925fa0..0000000 --- a/plugins/shared/skills/resources_gmail/SKILL.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -name: using-gmail-connector -description: Implements Gmail email reading, searching, and sending using generated clients and MCP tools. Use when doing ANYTHING that touches Gmail or email in any way, load this skill. ---- - -# Major Platform Resource: Gmail - -## Setting Up a Gmail Connector - -Gmail requires OAuth authentication before use. - -### When the user asks you to set up Gmail or connect their email: - -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "gmail"` — this prompts the user to authenticate with Google -2. Once setup completes, the resource is ready to use - ---- - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Description field:** Always include a short `description` (~5 words) when calling any resource MCP tool, explaining what the operation does (e.g. "Search recent emails", "Send meeting invite"). This is displayed to the user in the chat UI. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-recent-emails"`, never dynamic values like `` `${date}-emails` ``. - ---- - -## MCP Tools - -- `mcp__resources__gmail_list_messages` — Search and list emails. Args: `resourceId`, `q?` (Gmail search syntax), `maxResults?`, `pageToken?`. **Returns only message IDs and thread IDs** — always follow up with `gmail_get_message` for headers or content. -- `mcp__resources__gmail_get_message` — Get one message by ID. Args: `resourceId`, `messageId`, `format?` (default: `"full"`). Use `format="metadata"` when you only need headers, and the default `format="full"` for message content — it returns a **normalized** response (see below). Never reach for `gmail_invoke` to read an ordinary message. -- `mcp__resources__gmail_send_message` — Send a plain-text email. Args: `resourceId`, `to`, `subject`, `body`, `cc?`, `bcc?`. Requires the `readwrite` scope preset. -- `mcp__resources__gmail_list_labels` — List all Gmail labels (inbox, sent, custom labels). Args: `resourceId` -- `mcp__resources__gmail_invoke` — Escape hatch for Gmail API v1 operations the typed tools don't cover (complete threads, drafts, label modification, attachment bytes). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?`. Returns the **unmodified** Gmail response, so it is not bounded — see the fallback section below. - -### Reading messages - -Standard flow — two steps, no raw MIME, no manual decoding: - -1. `gmail_list_messages` → message IDs. -2. `gmail_get_message` per ID → `format="metadata"` for headers only, or the default `format="full"` for content. - -`format="full"` is normalized into an agent-friendly, size-bounded object at `body.value`: - -| Field (full path in the tool result) | Meaning | -| --- | --- | -| `body.value.headers` | Selected headers only: `from`, `to`, `cc`, `bcc`, `subject`, `date`, `reply-to`, `in-reply-to`, `message-id`. Each is `{ name, value }`. | -| `body.value.body.text` | The **already-decoded** message text. Prefers `text/plain`, falling back to `text/html`. | -| `body.value.body.mimeType` | Which of the two the text came from (`text/plain` or `text/html`). | -| `body.value.body.truncated` | `true` when the text was cut to fit the budget. | -| `body.value.body.originalChars` | Character count of the full decoded text before truncation, so you can tell how much you're missing. | -| `body.value.attachments` | Bounded metadata only — `filename`, `mimeType`, `size`, `attachmentId`. **Never base64 payload bytes.** | -| `body.value.attachmentsTruncated` | `true` when attachments were dropped to fit the budget. | - -(`body.value` is the message; `body.value.body` is its decoded content — the outer `body` is the HTTP response envelope.) - -The text is capped at 16,000 characters and the whole formatted response at roughly 20KB; when the response would still be too large, the body text is trimmed further and then attachments, labels, and headers are shed. Read the `truncated` / `originalChars` / `attachmentsTruncated` flags rather than assuming you got everything. - -```jsonc -// gmail_get_message(resourceId, messageId, format: "full") → -{ - "kind": "api", - "status": 200, - "body": { - "kind": "json", - "value": { - "id": "m1", - "threadId": "t1", - "labelIds": ["INBOX", "UNREAD"], - "snippet": "hi", - "headers": [ - { "name": "From", "value": "a@b.com" }, - { "name": "Subject", "value": "Hello" } - ], - "body": { "mimeType": "text/plain", "text": "Hello", "truncated": false, "originalChars": 5 }, - "attachments": [ - { "filename": "a.pdf", "mimeType": "application/pdf", "size": 10, "attachmentId": "att1" } - ], - "attachmentsTruncated": false - } - } -} -``` - -Because the text arrives decoded, **do not** base64-decode it, and do not write the response to a file to `Read`/`grep`/parse it back — read `body.value.body.text` directly from the tool result. - -Only `format="full"` is normalized. `metadata`, `minimal`, and `raw` pass through as the upstream Gmail shape, so `metadata` headers stay under `body.value.payload.headers` and `raw` still returns base64 (`raw` is rarely what you want — prefer `full`). Gmail error responses and non-2xx statuses are passed through untouched, so keep checking `status`. - -### When to fall back to `gmail_invoke` - -Reserve it for operations the typed tools don't cover — most commonly fetching a **complete thread**: - -```jsonc -gmail_invoke({ - resourceId, - method: "GET", - path: "users/me/threads/THREAD_ID", - query: { "format": ["metadata"] } // values are ARRAYS of strings -}) -``` - -`query` is `map[string][]string`: every value must be an array, so it's `{"format": ["metadata"]}`, never `{"format": "metadata"}`. Since `gmail_invoke` returns the raw Gmail response, a thread fetched with `format="full"` carries base64 MIME parts for every message and can be enormous — prefer `format=["metadata"]` to enumerate the thread, then `gmail_get_message` per message ID for bounded content. - -### If any Gmail tool result comes back as a file reference - -Regardless of which tool triggered it, do not `Read` the file into context. Use `jq` to pull out only what you need: - -```bash -# Headers only, across every message in a thread dump -jq '.messages[].payload.headers' /path/to/response.json - -# Just the plain-text body part of one message, still base64 (decode after) -jq -r '.payload.parts[] | select(.mimeType == "text/plain") | .body.data' /path/to/response.json - -# Count messages / list IDs without loading bodies -jq '.messages | map(.id)' /path/to/response.json -``` - -Filter with `jq` first, decode base64 (`| base64 -d`) only on the small slice you actually need, and never pipe the full file through `cat`/`Read`. - -## TypeScript Client - -```typescript -import { gmailClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -// All paths are relative to https://gmail.googleapis.com/gmail/v1/ - -// Search for recent emails -const result = await gmailClient.invoke("GET", "users/me/messages", "search-emails", { - query: { q: "is:unread from:team@company.com", maxResults: "10" }, -}); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const messages = result.result.body.value.messages; -} - -// Get a specific message. The client is a thin wrapper over the Gmail API, so this -// returns the RAW Gmail shape (base64 MIME parts under body.value.payload) — the -// normalization and size bounds described above apply to the gmail_get_message MCP -// tool, not to gmailClient.invoke. In app code, decode payload parts yourself, or -// request format=metadata when headers are enough. -const msgResult = await gmailClient.invoke("GET", "users/me/messages/MSG_ID", "get-message", { - query: { format: "metadata" }, -}); -``` - -## Tips - -- **All paths are relative to `https://gmail.googleapis.com/gmail/v1/`** — e.g. use `users/me/messages`, not the full URL. -- **Gmail search syntax**: `from:user@example.com`, `subject:meeting`, `after:2024/01/01`, `is:unread`, `has:attachment`, `label:INBOX`. Combine with spaces (AND) or `OR`. -- **Message format options**: `full` (default — normalized, bounded, decoded text), `metadata` (headers only), `minimal` (IDs only), `raw` (RFC 2822, base64). Only `full` is normalized. -- **Two-step read pattern**: `list_messages` returns only IDs → `get_message` with `format="metadata"` for headers or the default `format="full"` for content. Use `gmail_invoke` only for what the typed tools don't cover, e.g. complete threads. -- **File-reference responses**: if any Gmail tool result comes back as a file reference instead of inline JSON, use `jq` to extract the fields you need (see above) rather than reading the whole file. -- **Pagination**: Check `nextPageToken` in the response and pass it as `pageToken` to get the next page. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` -- **Scope presets**: "readonly" (read/search only) or "readwrite" (read/search + send). Send operations fail with 403 on readonly. -- **Common paths**: `users/me/messages` (list/search), `users/me/messages/{id}` (get), `users/me/messages/send` (send), `users/me/labels` (list labels), `users/me/threads` (list threads) - -**Docs**: [Gmail API Reference](https://developers.google.com/gmail/api/reference/rest) diff --git a/plugins/shared/skills/resources_google-analytics/SKILL.md b/plugins/shared/skills/resources_google-analytics/SKILL.md deleted file mode 100644 index 2e70561..0000000 --- a/plugins/shared/skills/resources_google-analytics/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: using-google-analytics-connector -description: Implements Google Analytics (GA4) reporting, metadata exploration, and account management using generated clients and MCP tools. Use when doing ANYTHING that touches Google Analytics or GA4 in any way, load this skill. ---- - -# Major Platform Resource: Google Analytics - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-page-views"`, never dynamic values like `` `${date}-report` ``. - ---- - -## MCP Tools - -- `mcp__resources__googleanalytics_run_report` — Run a GA4 report with dimensions, metrics, and date ranges. Args: `resourceId`, `dimensions`, `metrics`, `dateRanges`, `orderBys?`, `limit?`, `offset?` -- `mcp__resources__googleanalytics_get_metadata` — Get available dimensions and metrics for the property. Args: `resourceId` -- `mcp__resources__googleanalytics_run_realtime_report` — Run a realtime report showing live activity. Args: `resourceId`, `dimensions?`, `metrics`, `limit?` -- `mcp__resources__googleanalytics_list_accounts` — List all GA4 accounts. Args: `resourceId`, `pageSize?`, `pageToken?` -- `mcp__resources__googleanalytics_list_properties` — List GA4 properties, optionally by account. Args: `resourceId`, `accountId?`, `pageSize?`, `pageToken?` -- `mcp__resources__googleanalytics_list_data_streams` — List data streams for a property. Args: `resourceId`, `propertyId?`, `pageSize?`, `pageToken?` -- `mcp__resources__googleanalytics_invoke` — Execute any GA4 operation. Args: `resourceId`, `operation`, + operation-specific args - -## TypeScript Client - -```typescript -import { gaClient } from "./clients"; - -// runReport(dimensions, metrics, dateRanges, invocationKey, options?) -const result = await gaClient.runReport( - [{ name: "country" }, { name: "city" }], - [{ name: "activeUsers" }, { name: "sessions" }], - [{ startDate: "30daysAgo", endDate: "today" }], - "traffic-by-location", - { limit: 100 }, -); - -// getMetadata(invocationKey) -const metadata = await gaClient.getMetadata("discover-dimensions"); - -// listAccounts(invocationKey, options?) -const accounts = await gaClient.listAccounts("list-ga-accounts"); - -// listProperties(invocationKey, accountId?, options?) -const properties = await gaClient.listProperties("list-ga-properties", "accounts/12345"); - -// runRealtimeReport(metrics, invocationKey, dimensions?, limit?) -const realtime = await gaClient.runRealtimeReport([{ name: "activeUsers" }], "live-users"); -``` - -## Tips - -- **Property ID format**: Use the numeric GA4 property ID (e.g., `123456789`), not the measurement ID (G-XXXXXXXX). Find it in GA4 Admin > Property Settings. -- **Date ranges**: Use relative dates like `today`, `yesterday`, `7daysAgo`, `30daysAgo`, or absolute dates in `YYYY-MM-DD` format. -- **Discover available data**: Use `get_metadata` first to see which dimensions and metrics are available for the property before running reports. -- **Dimension/metric names**: Use API names like `country`, `city`, `activeUsers`, `sessions`, `screenPageViews` — not display names. -- **Realtime reports**: Do not support date ranges (they show live data only). Only a subset of dimensions/metrics are available. -- **Pagination**: Use `limit` and `offset` for report results, `pageSize` and `pageToken` for list operations. - -**Docs**: [GA4 Data API](https://developers.google.com/analytics/devguides/reporting/data/v1) | [GA4 Admin API](https://developers.google.com/analytics/devguides/config/admin/v1) diff --git a/plugins/shared/skills/resources_googlecalendar/SKILL.md b/plugins/shared/skills/resources_googlecalendar/SKILL.md deleted file mode 100644 index e08064d..0000000 --- a/plugins/shared/skills/resources_googlecalendar/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: using-googlecalendar-connector -description: Implements Google Calendar event management and scheduling using generated clients and MCP tools. Use when doing ANYTHING that touches Google Calendar or gcal in any way, load this skill. ---- - -# Major Platform Resource: Google Calendar - -## Setting Up a Google Calendar Connector - -Google Calendar requires OAuth authentication before use. - -### When the user asks you to set up Google Calendar or connect their calendar: - -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "googlecalendar"` — this prompts the user to authenticate with Google -2. Once setup completes, the resource is ready to use - ---- - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Description field:** Always include a short `description` (~5 words) when calling any resource MCP tool, explaining what the operation does (e.g. "List all user accounts", "Check table schema"). This is displayed to the user in the chat UI. - -**Three ways to interact with Google Calendar:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Google Calendar API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-upcoming-events"`, never dynamic values like `` `${date}-events` ``. - ---- - -## MCP Tools - -- `mcp__resources__googlecalendar_list_calendars` — List all calendars accessible to the connected account. Args: `resourceId` -- `mcp__resources__googlecalendar_list_events` — List events with optional date filtering, search, and pagination. Args: `resourceId`, `calendarId?` (default: "primary"), `timeMin?`, `timeMax?`, `maxResults?`, `q?`, `singleEvents?`, `orderBy?`, `pageToken?` -- `mcp__resources__googlecalendar_create_event` — Create a new calendar event. Args: `resourceId`, `summary`, `startDateTime`, `endDateTime`, `calendarId?`, `location?`, `eventDescription?`, `timeZone?`, `attendees?` -- `mcp__resources__googlecalendar_invoke` — Make any HTTP request to the Google Calendar API v3 (for operations not covered by other tools). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { googleCalendarClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -// All paths are relative to https://www.googleapis.com/calendar/v3/ - -// List upcoming events -const result = await googleCalendarClient.invoke("GET", "calendars/primary/events", "list-events", { - query: { timeMin: new Date().toISOString(), maxResults: "10", singleEvents: "true", orderBy: "startTime" }, -}); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const events = result.result.body.value.items; -} - -// Create an event -const createResult = await googleCalendarClient.invoke("POST", "calendars/primary/events", "create-meeting", { - body: { - type: "json", - value: { - summary: "Team Standup", - start: { dateTime: "2026-04-03T10:00:00-07:00" }, - end: { dateTime: "2026-04-03T10:30:00-07:00" }, - attendees: [{ email: "colleague@example.com" }], - }, - }, -}); -``` - -## Tips - -- **All paths are relative to `https://www.googleapis.com/calendar/v3/`** — e.g. use `calendars/primary/events`, not the full URL. -- **Use `singleEvents=true`** when listing events to expand recurring events into individual instances. This also enables `orderBy=startTime`. -- **Date format**: Use RFC3339 for datetime (`2026-04-02T10:00:00-07:00`) or `YYYY-MM-DD` for all-day events. -- **Default calendar**: Use `"primary"` as the calendar ID to target the user's main calendar. -- **Pagination**: Check `nextPageToken` in the response and pass it as `pageToken` to get the next page. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` -- **Common paths**: `calendars/primary/events` (list/create events), `users/me/calendarList` (list calendars), `calendars/{calendarId}/events/{eventId}` (get/update/delete event) -- **Scope presets**: The resource may be configured as "readonly" (can only read) or "readwrite" (can read and create/modify events). Write operations will fail with 403 if the resource is readonly. - -**Docs**: [Google Calendar API Reference](https://developers.google.com/calendar/api/v3/reference) diff --git a/plugins/shared/skills/resources_googledrive/SKILL.md b/plugins/shared/skills/resources_googledrive/SKILL.md deleted file mode 100644 index edcf0cf..0000000 --- a/plugins/shared/skills/resources_googledrive/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: using-googledrive-connector -description: Implements Google Drive file listing, reading, and management using generated clients and MCP tools. Use when doing ANYTHING that touches Google Drive, files, or documents in any way, load this skill. ---- - -# Major Platform Resource: Google Drive - -## Setting Up a Google Drive Connector - -Google Drive requires OAuth authentication before use. - -### When the user asks you to set up Google Drive or connect their files: - -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "googledrive"` — this prompts the user to authenticate with Google -2. Once setup completes, the resource is ready to use - ---- - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Description field:** Always include a short `description` (~5 words) when calling any resource MCP tool, explaining what the operation does (e.g. "List project documents", "Get file contents"). This is displayed to the user in the chat UI. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-project-files"`, never dynamic values like `` `${folder}-files` ``. - ---- - -## MCP Tools - -- `mcp__resources__googledrive_list_files` — Search and list files. Args: `resourceId`, `query?` (Drive search syntax), `maxResults?`, `pageToken?` -- `mcp__resources__googledrive_get_file` — Get file metadata by ID. Args: `resourceId`, `fileId` -- `mcp__resources__googledrive_get_file_content` — Export a Google Docs/Sheets/Slides file to a specified format. Args: `resourceId`, `fileId`, `mimeType?` (default: "text/plain"). For binary files, use `googledrive_invoke` with `alt=media`. -- `mcp__resources__googledrive_list_shared_drives` — List shared drives. Args: `resourceId` -- `mcp__resources__googledrive_invoke` — Make any HTTP request to the Google Drive API v3 (for operations not covered by other tools). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { googleDriveClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -// All paths are relative to https://www.googleapis.com/drive/v3/ - -// List recent files -const result = await googleDriveClient.invoke("GET", "files", "list-files", { - query: { pageSize: "10", fields: "files(id,name,mimeType,modifiedTime)", orderBy: "modifiedTime desc" }, -}); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const files = result.result.body.value.files; -} - -// Search for spreadsheets -const searchResult = await googleDriveClient.invoke("GET", "files", "search-spreadsheets", { - query: { q: "mimeType='application/vnd.google-apps.spreadsheet'", fields: "files(id,name)" }, -}); - -// Export a Google Doc as plain text -const exportResult = await googleDriveClient.invoke("GET", "files/FILE_ID/export", "export-doc", { - query: { mimeType: "text/plain" }, -}); -``` - -## Tips - -- **All paths are relative to `https://www.googleapis.com/drive/v3/`** — e.g. use `files`, not the full URL. -- **Drive search syntax**: `name contains 'report'`, `mimeType = 'application/vnd.google-apps.spreadsheet'`, `modifiedTime > '2024-01-01'`, `'FOLDER_ID' in parents`, `trashed = false`. Combine with `and`/`or`. -- **Google Workspace MIME types**: `application/vnd.google-apps.document` (Docs), `application/vnd.google-apps.spreadsheet` (Sheets), `application/vnd.google-apps.presentation` (Slides), `application/vnd.google-apps.folder` (Folder) -- **Export MIME types** (for `get_file_content`): `text/plain`, `text/csv`, `application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (.docx), `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (.xlsx) -- **Binary file download**: Use `googledrive_invoke` with path `files/{id}?alt=media` to download non-Google files directly. -- **Pagination**: Check `nextPageToken` in the response and pass it as `pageToken` to get the next page. -- **Fields parameter**: Use `fields` query param to limit response size, e.g. `fields=files(id,name,mimeType,modifiedTime,size)`. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` -- **Scope presets**: "readonly" (view files) or "readwrite" (view all files + manage app-created files). Write operations fail with 403 on readonly. - -**Docs**: [Google Drive API Reference](https://developers.google.com/drive/api/reference/rest/v3) diff --git a/plugins/shared/skills/resources_googlesearchconsole/SKILL.md b/plugins/shared/skills/resources_googlesearchconsole/SKILL.md deleted file mode 100644 index bc42003..0000000 --- a/plugins/shared/skills/resources_googlesearchconsole/SKILL.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: using-googlesearchconsole-connector -description: Implements Google Search Console data access for search analytics, sitemaps, sites, and URL inspection using generated clients and MCP tools. Use when doing ANYTHING that touches Google Search Console or SEO search data, load this skill. ---- - -# Major Platform Resource: Google Search Console - -Google Search Console is an OAuth-only connector. It accesses verified Search Console properties for the signed-in Google user. Do not describe or use it as an API-key/public URL testing connector. - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-top-queries"`, never dynamic values like `` `${date}-report` ``. - ---- - -## MCP Tools - -- `mcp__resources__googlesearchconsole_query_analytics` — Query search analytics data (clicks, impressions, CTR, position). Args: `resourceId`, `startDate`, `endDate`, `dimensions?`, `searchType?`, `dimensionFilterGroups?`, `rowLimit?`, `startRow?` -- `mcp__resources__googlesearchconsole_list_sites` — List all verified sites in Search Console. Args: `resourceId` -- `mcp__resources__googlesearchconsole_get_site` — Get info about a specific site. Args: `resourceId`, `siteUrl` -- `mcp__resources__googlesearchconsole_list_sitemaps` — List sitemaps for a site. Args: `resourceId`, `siteUrl` -- `mcp__resources__googlesearchconsole_invoke` with `operation: "getSitemap"` — Get one sitemap. Args: `resourceId`, `operation`, `siteUrl`, `feedpath` -- `mcp__resources__googlesearchconsole_inspect_url` — Inspect a URL's index status. Args: `resourceId`, `siteUrl`, `inspectionUrl` -- `mcp__resources__googlesearchconsole_invoke` — Execute any Search Console operation. Args: `resourceId`, `operation`, + operation-specific args - -## TypeScript Client - -```typescript -import { gscClient } from "./clients"; - -// queryAnalytics(startDate, endDate, invocationKey, options?) -const result = await gscClient.queryAnalytics("2024-01-01", "2024-01-31", "top-queries", { - dimensions: ["query", "page"], - rowLimit: 100, -}); - -// listSites(invocationKey) -const sites = await gscClient.listSites("list-gsc-sites"); - -// listSitemaps(siteUrl, invocationKey) -const sitemaps = await gscClient.listSitemaps("https://example.com/", "list-sitemaps"); - -// getSitemap(siteUrl, feedpath, invocationKey) -const sitemap = await gscClient.getSitemap( - "https://example.com/", - "https://example.com/sitemap.xml", - "get-sitemap", -); - -// inspectUrl(siteUrl, inspectionUrl, invocationKey) -const inspection = await gscClient.inspectUrl("https://example.com/", "https://example.com/page", "inspect-page"); -``` - -## Tips - -- **OAuth only**: This connector works with properties the authenticated Google user can access. Use `list_sites` first to discover verified properties. -- **Site URL formats**: Use either `https://example.com/` (URL prefix) or `sc-domain:example.com` (domain property). Pass the exact value Search Console returns. -- **Data delay**: GSC data is typically 2-3 days behind real-time. Don't expect yesterday's data to be complete. -- **Dimensions**: Available: `query`, `page`, `country`, `device`, `date`, `searchAppearance`. Include `date` to get daily breakdowns. -- **Search types**: `web` (default), `image`, `video`, `news`, `discover`, `googleNews`. -- **Row limit**: Max 25,000 rows per request. Use `startRow` for pagination. -- **Date format**: Use `YYYY-MM-DD` format for start/end dates. -- **Filter operators**: `contains`, `equals`, `notContains`, `notEquals`, `includingRegex`, `excludingRegex`. -- **URL Inspection**: Inspects one URL at a time. Rate-limited to 600 QPM/site. - -**Docs**: [Search Console API](https://developers.google.com/webmaster-tools/v1/api_reference_index) | [Search Analytics](https://developers.google.com/webmaster-tools/v1/searchanalytics) diff --git a/plugins/shared/skills/resources_googlesheets/SKILL.md b/plugins/shared/skills/resources_googlesheets/SKILL.md deleted file mode 100644 index 2db72ca..0000000 --- a/plugins/shared/skills/resources_googlesheets/SKILL.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: using-googlesheets-connector -description: Implements Google Sheets reading, writing, formatting, and batch operations using generated clients and MCP tools. Use when doing ANYTHING that touches Google Sheets or gsheets in any way, load this skill. ---- - -# Major Platform Resource: Google Sheets - -## Setting Up a Google Sheets Connector - -Google Sheets requires a two-step setup: (1) OAuth authentication, (2) spreadsheet selection. - -### When the user asks you to set up Google Sheets or connect a spreadsheet: - -1. Call `mcp__resource-setup__request-resource-setup` with `subtype: "googlesheets"` — this prompts the user to authenticate with Google -2. After setup completes, call `mcp__resource-setup__request-resource-update` with the returned `resourceId` and `message: "Please select your spreadsheet. Click 'Configure Resource' below, then use the spreadsheet picker to choose your sheet."` — this prompts them to select their spreadsheet -3. Once both steps complete, the resource is ready to use - -### When the user sends a Google Sheets link: - -If the user shares a Google Sheets URL (e.g., `https://docs.google.com/spreadsheets/d/...`), you cannot connect to it directly via the URL. Explain that they need to set up a Google Sheets connector: - -1. Tell them: "To connect to this spreadsheet, we need to set up a Google Sheets connector. This involves authenticating with Google and then selecting your spreadsheet." -2. Follow the setup flow above (steps 1-3) -3. After setup, the resource will be bound to the spreadsheet they select — remind them to pick the correct one - -### When a Google Sheets resource exists but has no spreadsheet selected: - -If you call a Google Sheets MCP tool and get an error indicating no spreadsheet is configured, use `mcp__resource-setup__request-resource-update` to prompt the user to select one. - ---- - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Google Sheets:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Google Sheets API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__googlesheets_get_metadata` — Get spreadsheet metadata (title, locale, sheet info). Args: `resourceId` -- `mcp__resources__googlesheets_get_values` — Read cell values from a range. Args: `resourceId`, `range` -- `mcp__resources__googlesheets_list_sheets` — List all sheets (tabs) with properties. Args: `resourceId` - -## TypeScript Client - -**Prefer helper methods over raw `invoke()`:** - -```typescript -import { sheetsClient } from "./clients"; - -// Read values -const values = await sheetsClient.getValues("Sheet1!A1:D10", "fetch-data"); - -// Append rows -await sheetsClient.appendValues("Sheet1!A:D", [["John", "Doe", "john@example.com", "2024-01-15"]], "append-row", { - valueInputOption: "USER_ENTERED", -}); - -// Update values -await sheetsClient.updateValues( - "Sheet1!A1:B2", - [ - ["Name", "Email"], - ["Jane", "jane@ex.com"], - ], - "update-cells", -); - -// Batch operations -await sheetsClient.batchGetValues(["Sheet1!A1:B5", "Sheet2!A1:C3"], "batch-read"); - -// Formatting via batchUpdate -await sheetsClient.batchUpdate( - [ - { - repeatCell: { - range: { sheetId: 0, startRowIndex: 0, endRowIndex: 1 }, - cell: { userEnteredFormat: { textFormat: { bold: true } } }, - fields: "userEnteredFormat.textFormat.bold", - }, - }, - ], - "bold-header", -); -``` - -## Tips - -- **Use helper methods** (`getValues`, `appendValues`, `updateValues`, `batchGetValues`, `batchUpdateValues`, `batchUpdate`) over raw `invoke()` when possible -- Each resource is bound to a single spreadsheet — the spreadsheet ID is automatically included -- For raw `invoke()`, paths are relative to `/v4/spreadsheets/{spreadsheetId}` (e.g., `/values/Sheet1!A1:D10`) -- Use `valueInputOption: "USER_ENTERED"` to let Sheets parse formulas and dates - -**Docs**: [Google Sheets API Reference](https://developers.google.com/workspace/sheets/api/reference/rest) diff --git a/plugins/shared/skills/resources_graphql/SKILL.md b/plugins/shared/skills/resources_graphql/SKILL.md deleted file mode 100644 index 491c8e2..0000000 --- a/plugins/shared/skills/resources_graphql/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: using-graphql-connector -description: Executes GraphQL queries and mutations against a configured endpoint using generated clients and MCP tools. Use when doing ANYTHING that touches a GraphQL resource in any way, load this skill. ---- - -# Major Platform Resource: GraphQL API - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with this resource:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **Apollo Client via the HTTP proxy** (GraphQL-specific): Pass `createProxyFetch({ resourceId, ... })` as Apollo's `fetch` so the proxy resolves the endpoint and injects auth at request time. Use this when the app needs Apollo's normalized cache, optimistic updates, or fragments. See "Apollo Client via the HTTP Proxy" below. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__graphql_query` — Execute a read-only GraphQL query. Args: `resourceId`, `query`, `variables?`, `operationName?`, `headers?`, `timeoutMs?` -- `mcp__resources__graphql_mutate` — Execute a GraphQL mutation (create, update, delete). Args: `resourceId`, `mutation`, `variables?`, `operationName?`, `headers?`, `timeoutMs?` -- `mcp__resources__graphql_introspect` — Fetch the full GraphQL schema via introspection. Args: `resourceId` - -## TypeScript Client - -```typescript -import { graphqlClient } from "./clients"; - -// query(query, invocationKey, options?) -const result = await graphqlClient.query( - `query GetUsers($limit: Int) { - users(limit: $limit) { - id - name - email - } - }`, - "list-users", - { variables: { limit: 10 }, headers: { "X-Request-ID": "abc-123" } }, -); -if (result.ok) { - const response = result.result; - // response: { kind: "api", status: number, body: { kind: "json", value: { data: { users: [...] } } } } -} - -// mutate(mutation, invocationKey, options?) -const createResult = await graphqlClient.mutate( - `mutation CreateUser($input: CreateUserInput!) { - createUser(input: $input) { - id - name - } - }`, - "create-user", - { variables: { input: { name: "Jane", email: "jane@example.com" } } }, -); -if (createResult.ok) { - console.log(createResult.result.body); -} -``` - -## Apollo Client via the HTTP Proxy - -For apps that need Apollo Client (normalized cache, optimistic updates, subscriptions over HTTP, etc.), wire it up by passing the proxy fetch as Apollo's `fetch`. The proxy injects the configured auth header upstream — the client code never sees credentials. - -```typescript -import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const client = new ApolloClient({ - link: new HttpLink({ - uri: () => "", // use the configured endpoint without adding a slash - fetch: createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: "", - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, - }), - }), - cache: new InMemoryCache(), -}); -``` - -**Key points:** - -- **`proxyFetch("")`** uses the configured endpoint without adding a slash. In Apollo use `uri: () => ""`; plain `uri: ""` falls back to `/graphql`. -- **Auth is injected server-side** — never set `Authorization` on the Apollo link; the proxy strips reserved request headers and replaces them with the resource's configured auth (`bearer`, `apiKey`, or `none`). -- **Resource ID must be static** — `createProxyFetch` is detected by the query extractor only when `resourceId` is a string literal; dynamic IDs are skipped at deploy time. -- **Next.js**: use Apollo on the server (RSC, Route Handlers, Server Actions) so the JWT stays out of the browser. For client-side Apollo, route the request through your own server endpoint. -- **Same auth/policy as MCP/client paths** — URLPolicy admits only the configured endpoint host + path, so misconfigured URIs 403 rather than leaking traffic elsewhere. - -## Tips - -- **Use variables** — always pass dynamic values via the `variables` parameter, never interpolate them into the query string -- **GraphQL returns 200 even on errors** — check `response.body.value.errors` in addition to HTTP status. GraphQL errors are returned in the response body with a 200 status code -- **Use introspection for schema discovery** — run `graphql_introspect` to see available types, queries, and mutations before writing queries -- **Auth headers are automatically injected** — the resource configuration includes the authentication header that you don't need to set manually -- **Per-request headers** — pass `headers` to include additional HTTP headers (e.g. `X-Request-ID`, tenant headers). Per-request headers override the resource's configured auth header if they use the same header name -- Both `query` and `mutate` methods accept the same shape; `mutate` is semantically separated for clarity and MCP read-only enforcement - -**Docs**: Refer to the specific GraphQL API's documentation for schema details. diff --git a/plugins/shared/skills/resources_hubspot/SEARCH.md b/plugins/shared/skills/resources_hubspot/SEARCH.md deleted file mode 100644 index 8ddf8a2..0000000 --- a/plugins/shared/skills/resources_hubspot/SEARCH.md +++ /dev/null @@ -1,203 +0,0 @@ -# HubSpot CRM Search API Reference - -**Endpoint:** `POST /crm/v3/objects/{objectType}/search` - -## Request Body - -All fields are optional. Send only what you need. - -```json -{ - "filterGroups": [], - "properties": ["prop1", "prop2"], - "sorts": [{ "propertyName": "createdate", "direction": "DESCENDING" }], - "limit": 20, - "after": "20", - "query": "search text" -} -``` - ---- - -## Filter Operators - -| Operator | Value field | Behavior | -| -------------------- | --------------------- | ------------------------------------------------------------ | -| `EQ` | `value` | Exact match (case-insensitive except enums) | -| `NEQ` | `value` | Not equal (case-insensitive except enums) | -| `LT` | `value` | Less than | -| `LTE` | `value` | Less than or equal | -| `GT` | `value` | Greater than | -| `GTE` | `value` | Greater than or equal | -| `BETWEEN` | `value` + `highValue` | Range (inclusive) | -| `IN` | `values` (array) | Matches any in list. **String values must be lowercase.** | -| `NOT_IN` | `values` (array) | Excludes all in list. **String values must be lowercase.** | -| `HAS_PROPERTY` | (none) | Property has any value | -| `NOT_HAS_PROPERTY` | (none) | Property is empty/null | -| `CONTAINS_TOKEN` | `value` | Token match. Supports `*` wildcards for partial matching. | -| `NOT_CONTAINS_TOKEN` | `value` | Excludes token. Supports `*` wildcards for partial matching. | - -**Common mistakes:** - -- Using `value` instead of `values` (array) for `IN`/`NOT_IN` -- Forgetting `highValue` for `BETWEEN` -- Passing a `value` for `HAS_PROPERTY`/`NOT_HAS_PROPERTY` (they take none) -- Not lowercasing string values for `IN`/`NOT_IN` -- Using `CONTAINS_TOKEN` without wildcards for partial matching — `"smith"` matches the whole token `"smith"` only, NOT `"Blacksmith"`. Use `"*smith*"` for partial matches. - ---- - -## FilterGroups Logic - -- Filters **within** a filterGroup = **AND** -- Multiple **filterGroups** = **OR** - -**Limits:** - -- Max **5** filterGroups -- Max **6** filters per group -- Max **18** filters total across all groups -- Exceeding any limit returns `VALIDATION_ERROR` - -```json -{ - "filterGroups": [ - { - "filters": [ - { "propertyName": "firstname", "operator": "EQ", "value": "Alice" }, - { "propertyName": "city", "operator": "EQ", "value": "Boston" } - ] - }, - { - "filters": [{ "propertyName": "email", "operator": "CONTAINS_TOKEN", "value": "*@example.com" }] - } - ] -} -``` - -This matches: (firstname=Alice AND city=Boston) OR (email contains @example.com) - ---- - -## Case Sensitivity - -| Context | Behavior | -| ------------------------------------ | ------------------------------------------- | -| Enumeration (dropdown) properties | **Always case-sensitive** for all operators | -| String properties with `IN`/`NOT_IN` | Values **must be lowercase** | -| All other string filters | Case-insensitive | - ---- - -## Date/Timestamp Values - -**CRITICAL: Use Unix epoch milliseconds as strings, NOT date strings.** - -| Property Type | Format | Notes | -| -------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ | -| **datetime** (e.g., `createdate`, `hs_lastmodifieddate`) | Unix ms as string: `"1642672800000"` | Any valid ms timestamp | -| **date-only** (no time component) | `YYYY-MM-DD` string OR Unix ms at **midnight UTC** | If using epoch ms, must be exactly midnight UTC or the date may be wrong | - -```json -{ - "filterGroups": [ - { - "filters": [ - { - "propertyName": "hs_lastmodifieddate", - "operator": "BETWEEN", - "value": "1579514400000", - "highValue": "1642672800000" - } - ] - } - ] -} -``` - -**In TypeScript (via Major resource client):** - -```typescript -const value = new Date("2024-01-01").getTime().toString(); // "1704067200000" - -const result = await hubspotClient.invoke("POST", "/crm/v3/objects/contacts/search", "search-contacts", { - body: { - filterGroups: [ - { - filters: [ - { - propertyName: "createdate", - operator: "GTE", - value: value, - }, - ], - }, - ], - properties: ["firstname", "lastname", "email"], - }, -}); -``` - ---- - -## Sorting - -- Only **1** sort rule per request -- `direction`: `ASCENDING` or `DESCENDING` -- Default (no sort): ordered by creation date, oldest first - -```json -{ "sorts": [{ "propertyName": "createdate", "direction": "DESCENDING" }] } -``` - ---- - -## Pagination - -- Default page size: **10** -- Max page size: **200** -- Max total results: **10,000** (paging beyond this returns 400) -- Use `paging.next.after` from the response as the `after` parameter for the next page -- When `paging.next.after` is absent, there are no more results - ---- - -## Searching by Associations - -Use the pseudo-property `associations.{objectType}`: - -```json -{ "propertyName": "associations.contact", "operator": "EQ", "value": "123" } -``` - -**Limitation:** Association searching is NOT supported for custom objects via search endpoints. - ---- - -## Searchable Objects - -Contacts, companies, deals, tickets, products, quotes, line items, orders, invoices, carts, leads, discounts, fees, taxes, deal splits, feedback submissions, payments, subscriptions, and custom objects. - -**Engagement objects:** calls, emails, meetings, notes, tasks. - -### Default Searchable Properties (for `query` parameter) - -| Object | Searchable Properties | -| --------- | -------------------------------------------------------------------------------------------------- | -| Contacts | `firstname`, `lastname`, `email`, `phone`, `hs_additional_emails`, `fax`, `mobilephone`, `company` | -| Companies | `website`, `phone`, `name`, `domain` | -| Deals | `dealname`, `pipeline`, `dealstage`, `description`, `dealtype` | -| Tickets | `subject`, `content`, `hs_pipeline_stage`, `hs_ticket_category`, `hs_ticket_id` | -| Products | `name`, `description`, `price`, `hs_sku` | - ---- - -## Limits & Gotchas - -- **Rate limit**: 5 requests/second per account for search (stricter than general API) -- **Request body max**: 3,000 characters -- **Newly created/updated objects** may take a few seconds to appear in search results -- **Archived objects** never appear in results -- **Cannot filter** engagement objects by `hs_body_preview_html`; emails also cannot filter by `hs_email_html` or `hs_body_preview` -- **Phone numbers** are normalized — omit country code when searching -- **Property names are case-sensitive** — use exact internal names from the CRM schema diff --git a/plugins/shared/skills/resources_hubspot/SKILL.md b/plugins/shared/skills/resources_hubspot/SKILL.md deleted file mode 100644 index 545d9c9..0000000 --- a/plugins/shared/skills/resources_hubspot/SKILL.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: using-hubspot-connector -description: Implements HubSpot CRM data access for contacts, companies, and deals using generated clients and MCP tools. Use when doing ANYTHING that touches HubSpot in any way, load this skill. ---- - -# Major Platform Resource: HubSpot - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with HubSpot:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the HubSpot API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__hubspot_get` — Make a GET request to any HubSpot API endpoint. Args: `resourceId`, `path`, `queryParams?` -- `mcp__resources__hubspot_list_contacts` — List contacts with optional property filtering. Args: `resourceId`, `properties?`, `limit?` -- `mcp__resources__hubspot_list_companies` — List companies with optional property filtering. Args: `resourceId`, `properties?`, `limit?` -- `mcp__resources__hubspot_list_deals` — List deals with optional property filtering. Args: `resourceId`, `properties?`, `limit?` - -## TypeScript Client - -```typescript -import { hubspotClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -const result = await hubspotClient.invoke("GET", "/crm/v3/objects/contacts", "fetch-contacts", { - query: { limit: "10", properties: "firstname,lastname,email" }, -}); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const contacts = result.result.body.value.results; -} -``` - -## CRM Search API — [SEARCH.md](./SEARCH.md) - -Read **SEARCH.md** before doing any CRM search. It covers: - -- Filter operators (syntax, `value` vs `values` array, common mistakes) -- FilterGroup AND/OR logic and hard limits (5 groups, 6 filters/group, 18 total) -- **Date filters: epoch milliseconds as strings, NOT date strings** -- Case sensitivity rules (enums, `IN`/`NOT_IN` lowercase requirement) -- Sorting (1 rule max), pagination (200/page max, 10K total max) -- Association searching, searchable properties per object, rate limits (5 req/s) - ---- - -## Tips - -- **Use batch API calls when possible** — reduces API calls and avoids rate limits -- **Rate limits**: General API = 100 requests per 10 seconds. **Search API = 5 requests per second** (stricter). Respect `Retry-After` headers. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` -- Specify `properties` parameter to fetch only needed fields — improves performance - -**Docs**: [HubSpot API Reference](https://developers.hubspot.com/docs/api/overview) diff --git a/plugins/shared/skills/resources_lambda/SKILL.md b/plugins/shared/skills/resources_lambda/SKILL.md deleted file mode 100644 index e14c558..0000000 --- a/plugins/shared/skills/resources_lambda/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: using-lambda-connector -description: Implements AWS Lambda function invocation and management using generated clients and MCP tools. Use when doing ANYTHING that touches Lambda or AWS Lambda in any way, load this skill. ---- - -# Major Platform Resource: AWS Lambda - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__lambda_list_functions` — List accessible Lambda functions. Args: `resourceId`, `maxItems?` -- `mcp__resources__lambda_get_function` — Get function config and code location. Args: `resourceId`, `functionName`, `qualifier?` -- `mcp__resources__lambda_invoke` — Invoke a function. Args: `resourceId`, `functionName`, `payload?`, `invocationType?`, `qualifier?` - -## TypeScript Client - -```typescript -import { lambdaClient } from "./clients"; - -// invoke(functionName, payload, invocationKey, options?) -const result = await lambdaClient.invoke("my-function", { userId: "123", action: "process" }, "invoke-processor", { - invocationType: "RequestResponse", -}); -if (result.ok) { - console.log(result.result); -} -``` - -## Tips - -- **Invocation types**: `RequestResponse` (synchronous, default), `Event` (async fire-and-forget), `DryRun` (validate without executing) -- Payload size limit: 6MB for synchronous, 256KB for async invocations -- Use `qualifier` to invoke a specific version or alias (defaults to `$LATEST`) - -**Docs**: [AWS Lambda Documentation](https://docs.aws.amazon.com/lambda/) diff --git a/plugins/shared/skills/resources_linkedin/SKILL.md b/plugins/shared/skills/resources_linkedin/SKILL.md deleted file mode 100644 index 9bea4aa..0000000 --- a/plugins/shared/skills/resources_linkedin/SKILL.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -name: using-linkedin-connector -description: Implements LinkedIn Marketing API access for ad accounts, campaigns, creatives, and ad analytics using generated clients and MCP tools. Use when doing ANYTHING that touches the LinkedIn Marketing API — ad accounts, campaigns, creatives, or ad analytics — in any way, load this skill. ---- - -# Major Platform Resource: LinkedIn Marketing API - -Reference: https://learn.microsoft.com/en-us/linkedin/marketing/ - -## Common: Interacting with Resources - -**Security**: Never connect directly to LinkedIn APIs with raw credentials. Never put OAuth tokens in code, logs, env vars, prompts, or user-visible output. Always use generated clients or MCP tools. - -**Three ways to interact with LinkedIn:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources__linkedin_`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the LinkedIn Marketing API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and exact signatures before writing app code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** - use descriptive literals like `"fetch-linkedin-campaigns"`, never dynamic values like `` `${accountId}-campaigns` ``. - ---- - -## Scope And Permissions - -The v1 LinkedIn Marketing API connector exposes the **Advertising API** surface. Lead Sync and Conversions API will ship as separate Major connectors when LinkedIn approval lands; do not assume they are reachable through this resource. - -Available scope sets: - -- Read only: `r_ads`, `r_ads_reporting` -- Read + write: `r_ads`, `r_ads_reporting`, `rw_ads` -- Custom: any subset of the above (admins pick at connector creation time) - -If LinkedIn refresh fails because the refresh token is revoked or expired, the connector call will fail and the admin will see a Reconnect prompt in the connector panel. Do not ask users for tokens — every LinkedIn call goes through the connector. - ---- - -## MCP Tools - -- `mcp__resources__linkedin_list_ad_accounts` - List accessible LinkedIn ad accounts. Args: `resourceId`, `status?`, `pageSize?`, `pageToken?` -- `mcp__resources__linkedin_list_campaigns` - List campaigns for an ad account. Args: `resourceId`, `adAccountId`, `status?`, `pageSize?`, `pageToken?` -- `mcp__resources__linkedin_list_creatives` - List creatives for an ad account or campaigns. Args: `resourceId`, `adAccountId`, `campaignIds?`, `pageSize?`, `pageToken?` -- `mcp__resources__linkedin_get_ad_analytics` - Fetch ad analytics. Args: `resourceId`, `adAccountId`, `dateRange`, `pivot`, `metrics?` -- `mcp__resources__linkedin_invoke` - Escape hatch for LinkedIn Marketing API requests. Args: `resourceId`, `method`, `path`, `query?`, `body?` - -Prefer typed list and analytics tools over `linkedin_invoke` when they cover the use case. - -When using `linkedin_invoke` directly, pass query values as arrays because the connector backend expects `Record`, for example `{ q: ["search"], pageSize: ["25"] }`. - ---- - -## TypeScript Client - -Use the typed helpers for common Advertising API workflows. Use `invoke()` only when a helper does not cover the endpoint. - -```typescript -import { linkedinClient } from "./clients"; - -const accounts = await linkedinClient.listAdAccounts("fetch-linkedin-ad-accounts", { - pageSize: 25, -}); - -if (!accounts.ok) { - throw new Error(accounts.error.message); -} - -const campaigns = await linkedinClient.listCampaigns("123456", "fetch-linkedin-campaigns", { - status: ["ACTIVE"], - pageSize: 25, -}); - -if (!campaigns.ok) { - throw new Error(campaigns.error.message); -} -``` - -For analytics from app code, request only the fields needed for the UI or report. - -```typescript -const analytics = await linkedinClient.getAdAnalytics( - "123456", - { start: "2026-04-01", end: "2026-04-30" }, - "fetch-linkedin-analytics", - { - pivot: "CAMPAIGN", - metrics: ["impressions", "clicks", "costInLocalCurrency"], - }, -); - -if (!analytics.ok) { - throw new Error(analytics.error.message); -} -``` - ---- - -## LinkedIn API Notes - -- Use numeric IDs returned by LinkedIn tools when available. The connector handles LinkedIn REST headers and URN formatting for typed tools. -- Ad account and campaign IDs may appear either as plain IDs or URNs in LinkedIn responses. Preserve IDs exactly unless the generated client documents a normalized field. -- Campaigns are listed from `/adAccounts/{adAccountId}/adCampaigns?q=search`; creatives are listed from `/adAccounts/{adAccountId}/creatives?q=criteria`. -- Analytics date ranges: pass singular `dateRange: { start: "YYYY-MM-DD", end: "YYYY-MM-DD" }` and singular `pivot: "ACCOUNT" | "CAMPAIGN" | "CREATIVE"`. Do not use LinkedIn's batched `dateRanges` or `pivots` query names with this connector. -- Analytics metrics can be expensive. Keep date ranges and fields narrow. -- LinkedIn Marketing API responses commonly return `{ elements, paging }`. - -**Docs**: [LinkedIn Marketing API](https://learn.microsoft.com/en-us/linkedin/marketing/) diff --git a/plugins/shared/skills/resources_linkedinads/SKILL.md b/plugins/shared/skills/resources_linkedinads/SKILL.md deleted file mode 100644 index 23725f2..0000000 --- a/plugins/shared/skills/resources_linkedinads/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: using-linkedinads-connector -description: Implements LinkedIn Marketing API data access for ad accounts, campaigns, creatives, and analytics using generated clients and MCP tools. Use when doing ANYTHING that touches LinkedIn Ads in any way, load this skill. ---- - -# Major Platform Resource: LinkedIn Marketing API - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-ad-accounts"`, never dynamic values. - ---- - -## MCP Tools - -- `mcp__resources__linkedinads_get` — Make a GET request to any LinkedIn Marketing API endpoint. Args: `resourceId`, `path`, `query?` -- `mcp__resources__linkedinads_list_ad_accounts` — List ad accounts. Args: `resourceId`, `count?`, `start?` -- `mcp__resources__linkedinads_list_campaigns` — List campaigns for an ad account. Args: `resourceId`, `accountId`, `count?`, `start?` -- `mcp__resources__linkedinads_get_campaign_analytics` — Get campaign analytics. Args: `resourceId`, `accountId`, `campaignIds?`, `startDate`, `endDate`, `fields?` -- `mcp__resources__linkedinads_invoke` — Make any HTTP request (POST/PUT/PATCH/DELETE). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { linkedinAdsClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -const result = await linkedinAdsClient.invoke("GET", "/rest/adAccounts", "list-ad-accounts", { - query: { q: "search", count: "10" }, -}); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const accounts = result.result.body.value.elements; -} -``` - ---- - -## Tips - -- **LinkedIn Marketing API uses versioned headers** — Include `LinkedIn-Version: YYYYMM` header (the connector handles this automatically). The API version is set in the handler. -- **URN format**: LinkedIn uses URNs like `urn:li:sponsoredAccount:123456` for resource identifiers. -- **Pagination**: Use `start` and `count` query params. Response includes `paging.total` for total count. -- **Rate limits**: Varies by endpoint. Respect `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers. -- **Analytics date format**: Use ISO 8601 dates. The `dateRange` parameter uses `start.day`, `start.month`, `start.year` format. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` - -**Docs**: [LinkedIn Marketing API Reference](https://learn.microsoft.com/en-us/linkedin/marketing/) diff --git a/plugins/shared/skills/resources_list-resources/SKILL.md b/plugins/shared/skills/resources_list-resources/SKILL.md deleted file mode 100644 index 285be62..0000000 --- a/plugins/shared/skills/resources_list-resources/SKILL.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: list-resources -description: Use this skill whenever you need to discover or work with resources (databases, APIs, storage, etc.) available to the application. Load this skill before doing any work that involves resources. ---- - -# Discovering and Using Resources - -The application has been granted access to resources — external services (databases, APIs, storage, etc.) that your application can interact with through Major's secure clients. - -## Step 1: List available resources - -Call `mcp__resources__list_resources` to get all resources the application has access to. - -## Step 2: Check for context documents before doing any work - -**Always call `mcp__resources__list_resource_context` for every resource before doing any other work with it.** Resources often have context documents attached (API docs, schema references, usage guides) that tell you exactly how to use them. - -For each resource you plan to use: - -1. Call `mcp__resources__list_resource_context` with the `resourceId` immediately after listing resources -2. **If documents exist, you MUST read them before doing anything else with the resource.** Do NOT skip this step. Do NOT query the resource directly until you have read all relevant context documents. The user uploaded these documents specifically to guide how you use the resource. -3. For each relevant document, spawn the `file-reader` agent to download and read it: - ``` - Task tool with subagent_type: "file-reader" - Prompt: "Download and read this resource context document. - resourceId: , documentId: , filename: - Extract: " - ``` - The agent will download the file, read it, and return a summary plus the local file path. -4. If the context document contains schema or API information, use it directly — do not make redundant queries (e.g. do not run `\d` table commands if the schema is already in the context doc) -5. Tell the user which context documents you read and what you learned, so they know their context is being used diff --git a/plugins/shared/skills/resources_majorauth/SKILL.md b/plugins/shared/skills/resources_majorauth/SKILL.md deleted file mode 100644 index 6016780..0000000 --- a/plugins/shared/skills/resources_majorauth/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: using-auth-connector -description: Share or revoke application access for users by email. Use whenever the app needs to manage user access to the app. ---- - -# Major Platform Resource: Major Auth Connector - -## Common: Interacting with Resources - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"share-user-access"`, never dynamic values like `` `${date}-share` ``. - ---- - -## How to Use - -The Major Auth Connector is a **managed resource** that exists by default in every organization. To use it: - -1. Call `mcp__resources__list_resources` to discover available resources — look for the one with subtype `majorauth` (named "Major Auth Connector"). -2. Call `mcp__resource-tools__add-resource-client` with that `resourceId` to generate a typed `MajorAuthResourceClient`. -3. Use the generated client in your app code to share or revoke access. - -## MCP Tools - -- `mcp__resources__majorauth_share_access` — Grant a user view access to the current app by email. Creates user account and org membership if needed. Args: `resourceId`, `email` -- `mcp__resources__majorauth_revoke_access` — Revoke a user's view access to the current app by email. Only removes app-level access; does not affect org membership. Args: `resourceId`, `email` - -## TypeScript Client - -```typescript -import { authClient } from "./clients"; - -// Grant a user view access to the app by email -// Creates their account and org membership if they don't exist -const shareResult = await authClient.shareAccess("user@example.com", "share-user-access"); -if (shareResult.ok) { - console.log("Access granted:", shareResult.result.success); -} - -// Revoke a user's app access (does NOT remove them from the org) -const revokeResult = await authClient.revokeAccess("user@example.com", "revoke-user-access"); -if (revokeResult.ok) { - console.log("Access revoked:", revokeResult.result.success); -} -``` - -## Tips - -- **View access only**: `shareAccess` grants `Application:User` role which gives view access to the app. It does not grant edit or admin permissions. -- **Auto-creates users**: If the email doesn't have an account, one is automatically created and added to the organization. -- **Idempotent**: Calling `shareAccess` for a user who already has access is safe and will not error. -- **Revoke is app-scoped**: `revokeAccess` only removes the user's access to this specific app. It does not remove them from the organization or affect any other apps. -- **If a user is NOT invited through this connector, they will get redirected out of the app when they try to enter the app** diff --git a/plugins/shared/skills/resources_managed-database/SKILL.md b/plugins/shared/skills/resources_managed-database/SKILL.md deleted file mode 100644 index ea6e1f6..0000000 --- a/plugins/shared/skills/resources_managed-database/SKILL.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: using-managed-database -description: Set up and use Major-managed PostgreSQL databases. Use when the user wants a database, needs to store data, mentions "managed database", or asks about database setup. ---- - -# Major Platform: Managed Databases - -## What is a Managed Database? - -A managed database is a Major-hosted PostgreSQL instance provisioned and managed entirely by the platform. No credentials to manage, no connection strings to configure — everything is handled automatically. Once active, it appears as a regular PostgreSQL resource with `isManaged: true`. - -## App-Scoped vs Org-Scoped - -There are two types of managed databases: - -- **App-scoped** (created by this tool): Belongs to a single application. Permissions are automatically inherited from app roles — app admins and editors get `Resource:Admin` on the database. Other apps cannot access it. -- **Org-scoped**: Shared across all apps in the organization. Created by org admins through the dashboard. Visible to all apps with appropriate permissions. - -The `setup_managed_database` MCP tool creates **app-scoped** databases only. - -## Setting Up a Managed Database - -Call `mcp__resources__setup_managed_database` — no arguments needed. The tool automatically provisions a database for the current application. - -**Behavior:** - -- **First call** (no database exists): Starts provisioning. Takes around 1 minute. -- **While provisioning**: Returns status. Wait ~1 minute and call again. -- **Once active**: Returns the resource ID. The database is ready to use. -- **If failed**: Returns failure status. Deprovision and try again. - -## Using the Database Once Active - -After setup completes and you have the resource ID: - -1. **MCP tools** (direct SQL, no code needed): - - `mcp__resources__postgresql_psql` — Read-only SQL queries and psql commands (`\dt`, `\d`, etc.). Args: `resourceId`, `command` - - `mcp__orchestrator-platform__run_migration` — DDL/DML migrations (managed databases only). Args: `applicationId`, `migration`, `description` - -2. **Generated TypeScript clients** (for app code): - - Call `mcp__resource-tools__add-resource-client` with the `resourceId` to generate a typed PostgreSQL client - - Use the client for read/write operations in your application code - -## Identifying Managed Databases - -In `mcp__resources__list_resources`, managed databases have `isManaged: true` and a `managedScope` field: - -- `managedScope: "app"` — App-scoped, belongs to this application only -- `managedScope: "org"` — Org-scoped, shared across all apps in the organization - -The `run_migration` tool only works on managed databases. Regular (external) PostgreSQL resources have `isManaged: false`. - -## Choosing Between App and Org Databases - -If the user has both an app-scoped and an org-scoped managed database available, **ask the user which one they want to use** before proceeding. Do not assume. For example: "I see you have both an app database and an organization-wide database. Which one should I use for this task?" - -If there is only an app-scoped database just use that one, don't ask. If there is only an org-scoped database, ask the user if they'd like to make a new app-scoped db. Generally, it's better to use an app DB unless there's a real -reason that data that should be shared for the entire org. - -## Tips - -- Use `postgresql_psql` for read-only exploration (schema inspection, SELECT queries) -- Use `run_migration` for all schema changes and data modifications on managed databases -- Use parameterized queries (`$1`, `$2`, ...) — never interpolate values into SQL strings -- After creating tables with `run_migration`, generate a TypeScript client for the app to use in code diff --git a/plugins/shared/skills/resources_managed-file-storage/SKILL.md b/plugins/shared/skills/resources_managed-file-storage/SKILL.md deleted file mode 100644 index c081c78..0000000 --- a/plugins/shared/skills/resources_managed-file-storage/SKILL.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -name: using-managed-file-storage -description: Set up and use Major-managed file storage (object/blob storage) for app uploads, downloads, images, documents, and attachments. This is the DEFAULT, Major-native way to store files — prefer it over Amazon S3, SharePoint, Google Sheets, or any other resource when the user just wants "file storage". Use when the user mentions storing files, uploads, images, documents, attachments, avatars, or asks what storage options exist. ---- - -# Major Platform: Managed File Storage - -## What is Managed File Storage? - -Managed file storage is Major-hosted object storage provisioned and managed entirely by the platform (backed by S3). No bucket to create, no credentials to manage — everything is handled automatically. It is **org-level**: one store can serve multiple apps. - -Customers see a flat key namespace (e.g. `user/avatar.png`); the underlying bucket and per-tenant prefix are handled by Major and never exposed. - -**When the user asks "what can I use to store files?", managed file storage is the answer.** Mention it first. Amazon S3, SharePoint/OneDrive, Google Sheets, and Notion are NOT general-purpose file storage — only suggest them if the user explicitly needs that specific external system. - -## Setting It Up - -Managed file storage is **not** offered through `request-resource-setup` — that tool only covers connectors set up via the standard Add-Connector dialog, and file storage is provisioned differently. Do not try to set it up that way; it will not appear. Use the dedicated tools instead: - -- `mcp__resources__list_managed_file_stores` — list existing file stores in the org. **Always call this first** — reuse an existing store if one fits the use case. -- `mcp__resources__provision_managed_file_store` — create a new org-level file store. Synchronous; returns `{ resourceId, name }` immediately. Args: `name`. The caller is auto-granted `Resource:Admin`; the All Builders group gets `Resource:Builder`, so any builder in the org can use it. - -Once you have a `resourceId`, use it directly with the tools and client below. - -## Using It Once Provisioned - -1. **MCP tools** (direct, no code needed): - - `mcp__resources__blob_list` — list objects under a prefix. Args: `resourceId`, `prefix?`, `delimiter?`, `maxKeys?`, `continuationToken?` - - `mcp__resources__blob_get` — read an object's body + metadata. Args: `resourceId`, `key` - - `mcp__resources__blob_put` — write an object (`body` base64-encoded). Args: `resourceId`, `key`, `body`, `contentType?`, `cacheControl?`, `contentDisposition?` - - `mcp__resources__blob_del` — delete an object. Args: `resourceId`, `key` - -2. **Generated TypeScript client** (for app code): - - Call `mcp__resource-tools__add-resource-client` with the `resourceId` to generate a typed client into `/clients/` (Next.js) or `/src/clients/` (Vite). - - **The `resourceType` you pass MUST be `"blob"`** — that is the underlying resource subtype. It is NOT `"managed_file_store"` / `"managed-file-storage"`; those are only the product name and will fail with `Invalid type`. The generated client class is `BlobResourceClient`. - -**CRITICAL: Do NOT guess client method names or signatures.** ALWAYS read the actual generated client source (or the `@major-tech/resource-client` package) before writing client code. - -**Framework note**: Next.js = use the client in server-side code only (Server Components, Server Actions, Route Handlers). Vite = call directly from the frontend. - -**Error handling**: always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static string literals** — e.g. `"save-user-avatar"`, never `` `${userId}-avatar` ``. - -```typescript -import { blobClient } from "./clients"; - -// Small objects: inline put/get (body base64-encoded under the hood, ~few MB ceiling) -await blobClient.put("user/avatar.png", fileBytes, "save-user-avatar", { contentType: "image/png" }); - -const result = await blobClient.get("user/avatar.png", "read-user-avatar"); -if (result.ok) { - // result.result holds the object body + metadata -} - -// List under a prefix; delimiter "/" gives folder-like grouping -await blobClient.list("user/", "list-user-files", { delimiter: "/" }); - -await blobClient.del("user/avatar.png", "delete-user-avatar"); -``` - -## Tips - -- **Large files**: don't use inline `put`/`get` (capped at `BLOB_INLINE_MAX_BYTES`). Use `getUploadUrl(key, ...)` / `getDownloadUrl(key, ...)` to get a presigned URL, then PUT/GET directly against it. Upload URLs default to 15 min (max 1 hour); download URLs default to 1 hour (max 7 days). -- **Metadata only**: `getMetadata(key, ...)` returns size/content-type/etag/last-modified without downloading the body. -- **Keys are flat**: there are no real directories — `delimiter: "/"` emulates folder listings via common prefixes. -- **Content type matters** for browser rendering — set `contentType` on `put` / `getUploadUrl`. -- All stores are **org-level**: check `list_managed_file_stores` before provisioning a new one to avoid duplicates. diff --git a/plugins/shared/skills/resources_mcp_custom/SKILL.md b/plugins/shared/skills/resources_mcp_custom/SKILL.md deleted file mode 100644 index 7d7b599..0000000 --- a/plugins/shared/skills/resources_mcp_custom/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: using-mcp-custom-connector -description: Implements runtime tool calls to a custom (bring-your-own) remote MCP server connector, using the in-session MCP tools for exploration and the generic createMcpClient for app code. Use when doing ANYTHING that touches a custom MCP connector / remote MCP server / mcp_custom resource in any way, load this skill. ---- - -# Major Platform Resource: Custom MCP Connector (BYO remote MCP server) - -A custom MCP connector points at an external remote MCP server you bring. Its tools are defined by that upstream server, not by Major — so there is **no fixed `mcp__resources__mcp_custom_*` tool set**. The available tools, their names, and their argument shapes all come from the upstream server. - -## Common: Interacting with Resources - -**Security**: Never connect directly to the upstream MCP server. Never put credentials in code. The connector's auth is injected server-side using the resolved shared or per-user credential. - -**Two ways to interact with a custom MCP connector:** - -1. **In-session MCP tools** (direct, no code needed): the connector is mounted as its own MCP server, so its tools are callable during the build as `mcp____`. Use `mcp__resources__list_resources` to find the connector and its `resourceId`. Call these tools to discover what the upstream server exposes and to test behavior. -2. **Generic MCP client** (for app code): import `createMcpClient` from `@major-tech/resource-client/next` and call `.callTool()`. There is **no per-resource generation step** — it's one client reused for any MCP connector; you just pass the `resourceId`. - -**CRITICAL: Do NOT guess tool names or argument shapes.** They are defined by the upstream MCP server, not by Major or by the client — `callTool(name, args)` is a single generic method with no per-tool typed methods. Discover the real tool names and arg shapes from the in-session `mcp____*` tools before wiring them into app code. - -**Server-side only**: the app JWT must never reach the browser, so call `createMcpClient` from Next server code (Server Components, Server Actions, Route Handlers). - -**App context required**: `callTool()` needs the app's `baseUrl` / `applicationId` / JWT, which are injected into the deployed app's environment. In a coding session, reach the connector through the `mcp____*` tools instead. - -**Error handling**: unlike other resource clients, `callTool()` **throws** `ResourceInvokeError` on a transport failure or an error response — there is **no `result.ok` envelope** to check. Wrap calls in `try/catch`. A tool-level failure that the upstream reports successfully comes back as a normal result with `isError: true`. - ---- - -## MCP Tools (in-session) - -There is no static tool list. The connector's upstream tools are mounted as `mcp____`, where `` is the connector's mount slug. Examples depend entirely on the upstream server (e.g. `mcp__acme__search_tickets`, `mcp__acme__create_ticket`). Use `mcp__resources__list_resources` to find the connector, then call its mounted tools to learn the exact names and arguments. - -## TypeScript Client - -```typescript -import { createMcpClient } from "@major-tech/resource-client/next"; - -// Config comes from the deployed app's injected env; resourceId is the connector. -const mcp = createMcpClient({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - applicationId: process.env.APPLICATION_ID!, - resourceId: "", - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -// callTool(tool, args?) — `tool` is the upstream MCP tool name; `args` is -// forwarded verbatim. Throws ResourceInvokeError on transport/server error. -try { - const result = await mcp.callTool<{ tickets: Ticket[] }>( - "search_tickets", - { customerId, limit: 20 }, - ); - - if (result.isError) { - // the tool itself reported an error — detail is in result.content - console.error(result.content); - return; - } - - const data = result.structuredContent; // typed as { tickets: Ticket[] } | undefined - // or read result.content (text/other blocks) when the tool returns no structured payload -} catch (err) { - // ResourceInvokeError: transport failure or an error envelope from the proxy -} -``` - -## Tips - -- **Result shape** mirrors the MCP `CallToolResult`: read `result.structuredContent` (typed via the `` you pass) for structured payloads, or `result.content` for unstructured blocks; check `result.isError` for tool-level failures. -- **Args are forwarded verbatim** to the upstream tool — match the upstream server's expected schema exactly. -- **Auth is injected server-side** using the resolved shared or per-user credential; never set auth headers yourself. diff --git a/plugins/shared/skills/resources_metamarketing/SKILL.md b/plugins/shared/skills/resources_metamarketing/SKILL.md deleted file mode 100644 index 0a1eec7..0000000 --- a/plugins/shared/skills/resources_metamarketing/SKILL.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: using-metamarketing-connector -description: Implements Meta (Facebook) Marketing API access for campaigns, ads, insights, and lead forms using generated clients and MCP tools. Use when doing ANYTHING that touches Meta Marketing, Facebook Ads, or Instagram Ads in any way, load this skill. ---- - -# Major Platform Resource: Meta Marketing - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Meta Marketing:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Meta Marketing API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.json`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-campaign-insights"`, never dynamic values like `` `${date}-insights` ``. - ---- - -## MCP Tools - -- `mcp__resources__metamarketing_get` — Make a GET request to any Meta Marketing API endpoint. Args: `resourceId`, `path`, `query?` -- `mcp__resources__metamarketing_get_campaigns` — List campaigns for an ad account. Args: `resourceId`, `adAccountId`, `fields?`, `limit?`, `after?` -- `mcp__resources__metamarketing_get_adsets` — List ad sets for an ad account. Args: `resourceId`, `adAccountId`, `fields?`, `limit?`, `after?` -- `mcp__resources__metamarketing_get_ads` — List ads for an ad account. Args: `resourceId`, `adAccountId`, `fields?`, `limit?`, `after?` -- `mcp__resources__metamarketing_get_insights` — Get performance insights/analytics for an ad account. Args: `resourceId`, `adAccountId`, `fields?`, `datePreset?`, `timeRange?`, `level?`, `limit?` -- `mcp__resources__metamarketing_get_lead_forms` — List lead generation forms for a Facebook page. Args: `resourceId`, `pageId`, `limit?`, `after?` -- `mcp__resources__metamarketing_get_leads` — Get leads submitted to a lead form. Args: `resourceId`, `formId`, `limit?`, `after?` -- `mcp__resources__metamarketing_invoke` — Make any HTTP request (GET/POST/DELETE) for write operations. Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -This client uses the Stripe-style flattened invoke pattern — check `.ok`, then access `.json` directly (no need to dig through `result.body.kind`/`result.body.value`). - -```typescript -import { metamarketingClient } from "./clients"; - -// List campaigns for an ad account -const result = await metamarketingClient.invoke("GET", "/v21.0/act_123456/campaigns", "list-campaigns", { - query: { fields: "id,name,status,objective,daily_budget", limit: "25" }, -}); -if (result.ok) { - const campaigns = result.json.data; // typed as T (default unknown) -} - -// Get campaign insights with date filtering -const insights = await metamarketingClient.invoke("GET", "/v21.0/act_123456/insights", "get-insights", { - query: { - fields: "campaign_name,impressions,clicks,spend,ctr,cpc", - date_preset: "last_30d", - level: "campaign", - }, -}); -if (insights.ok) { - const rows = insights.json.data; -} - -// Get leads from a lead form -const leads = await metamarketingClient.invoke("GET", "/v21.0/1234567890/leads", "get-form-leads", { - query: { fields: "id,created_time,field_data" }, -}); -if (leads.ok) { - const leadRecords = leads.json.data; -} - -// Create a campaign (requires campaign_management or lead_forms scope preset) -const newCampaign = await metamarketingClient.invoke("POST", "/v21.0/act_123456/campaigns", "create-campaign", { - body: { - type: "json", - value: { - name: "Summer Sale 2025", - objective: "OUTCOME_TRAFFIC", - status: "PAUSED", - special_ad_categories: [], - }, - }, -}); -if (newCampaign.ok) { - const campaignId = newCampaign.json.id; -} -``` - -## Tips - -- **Ad account IDs use the `act_` prefix** — always include it, e.g. `act_123456789`. The ad account ID is configured per environment as `adAccountId`. -- **Graph API versioning**: All paths include a version prefix like `/v21.0/`. The connector auto-prefixes `/v21.0` if the path starts with `/` but not `/v`. -- **Field selection**: Meta's API returns minimal fields by default. Always pass a `fields` query parameter to request specific fields (e.g., `fields: "id,name,status,insights{impressions,clicks}"`). -- **Pagination**: Meta uses cursor-based pagination. Responses include a `paging` object with `cursors.after` and `cursors.before`. Pass `after` as a query parameter for the next page. Check for `paging.next` to know if more pages exist. -- **Rate limiting**: The Marketing API has rate limits tied to the ad account. Responses include `x-business-use-case-usage` headers. Respect 429 responses and back off. -- **Insights date presets**: Use `date_preset` for common ranges: `today`, `yesterday`, `last_7d`, `last_14d`, `last_30d`, `this_month`, `last_month`, `last_90d`. Or use `time_range` with `{"since":"2025-01-01","until":"2025-01-31"}`. -- **Insights levels**: The `level` parameter controls aggregation: `ad`, `adset`, `campaign`, `account`. -- **Scope tiers build on each other**: `campaign_analytics` (read-only) → `campaign_management` (read/write) → `lead_forms` (everything). Requesting a higher tier implicitly grants all lower-tier permissions. -- **Common endpoints**: - - `/v21.0/act_{id}/campaigns` — list campaigns - - `/v21.0/act_{id}/adsets` — list ad sets - - `/v21.0/act_{id}/ads` — list ads - - `/v21.0/act_{id}/insights` — account-level insights - - `/v21.0/{campaign_id}/insights` — campaign-level insights - - `/v21.0/{page_id}/leadgen_forms` — lead forms for a page - - `/v21.0/{form_id}/leads` — leads for a form - - `/v21.0/me/adaccounts` — list ad accounts for the authenticated user - -**Docs**: [Meta Marketing API Reference](https://developers.facebook.com/docs/marketing-apis) diff --git a/plugins/shared/skills/resources_mssql/SKILL.md b/plugins/shared/skills/resources_mssql/SKILL.md deleted file mode 100644 index ddd6112..0000000 --- a/plugins/shared/skills/resources_mssql/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: using-mssql-connector -description: Implements Microsoft SQL Server connections, queries, and schema exploration using generated clients and MCP tools. Use when doing ANYTHING that touches MSSQL, SQL Server, or T-SQL in any way, load this skill. ---- - -# Major Platform Resource: Microsoft SQL Server - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__mssql_list_schemas` — List all schemas (excludes system schemas). Args: `resourceId` -- `mcp__resources__mssql_list_tables` — List tables, optionally filtered by schema. Args: `resourceId`, `schema?` -- `mcp__resources__mssql_list_columns` — List columns with types/constraints for a table. Args: `resourceId`, `schema`, `table` -- `mcp__resources__mssql_query` — Execute read-only SQL (SELECT/WITH only). Args: `resourceId`, `statement`, `params?` - -## TypeScript Client - -```typescript -import { myMssqlClient } from "./clients"; - -// invoke(sql, params?, invocationKey, timeoutMs?) -// Uses named parameters: @paramName -const result = await myMssqlClient.invoke<{ id: number; name: string }>( - "SELECT * FROM users WHERE id = @id", - { id: userId }, - "fetch-user", -); -if (result.ok) { - console.log(result.result.rows); -} -``` - -## Tips - -- Uses **named parameters** (`@id`, `@name`) not positional (`$1`) -- MCP query tool only allows SELECT/WITH — no multi-statement queries -- Use `list_schemas` → `list_tables` → `list_columns` to explore database structure - -**Docs**: [SQL Server Documentation](https://learn.microsoft.com/en-us/sql/sql-server/) diff --git a/plugins/shared/skills/resources_mysql/SKILL.md b/plugins/shared/skills/resources_mysql/SKILL.md deleted file mode 100644 index 09eceaf..0000000 --- a/plugins/shared/skills/resources_mysql/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: using-mysql-connector -description: Implements MySQL database connections, SQL queries, and data operations using generated clients and MCP tools. Use when doing ANYTHING that touches MySQL in any way, load this skill. ---- - -# Major Platform Resource: MySQL - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__mysql_query` — Execute a read-only MySQL query. Supports SELECT and introspection statements like `SHOW TABLES`, `DESCRIBE table_name`, `SHOW DATABASES`, `SHOW CREATE TABLE table_name`, and `EXPLAIN`. **Does not require user approval — prefer this tool for all read-only operations.** Args: `resourceId`, `statement`, `params?`, `description`, `timeoutMs?` -- `mcp__resources__mysql_invoke` — Execute any SQL statement including write operations (INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE, DROP, etc.). Returns rows and rowsAffected. **Requires user approval — only use when you need to write data.** Args: `resourceId`, `sql`, `params?`, `description`, `timeoutMs?` - -**IMPORTANT: Always use `mysql_query` for read-only operations.** It does not require user approval, making the workflow faster and smoother. Only use `mysql_invoke` when you actually need to perform writes (INSERT, UPDATE, DELETE, DDL). Never use `mysql_invoke` for SELECT queries or schema exploration. - -## TypeScript Client - -```typescript -import { myMysqlClient } from "./clients"; - -// invoke(sql, params?, invocationKey, timeoutMs?) -// Uses positional ? placeholders -const result = await myMysqlClient.invoke<{ id: number; name: string }>( - "SELECT * FROM users WHERE id = ?", - [userId], - "fetch-user", -); -if (result.ok) { - console.log(result.result.rows); -} -``` - -## Tips - -- **Use `mysql_query` exclusively for read-only tasks. Never use `mysql_invoke` for read-only.** -- Uses **positional `?` placeholders** — not `$1, $2` like PostgreSQL or `@name` like MSSQL. The first `?` maps to `params[0]`, the second to `params[1]`, etc. -- Default port is **3306** -- Use `mysql_query` with `SHOW DATABASES`, `SHOW TABLES`, `DESCRIBE table_name`, `SHOW CREATE TABLE table_name`, `SHOW INDEX FROM table_name` to explore database structure -- JSON columns (MySQL 5.7+) are returned as parsed objects, not raw strings -- Use backticks (`` ` ``) for identifiers (table/column names), single quotes for string values -- `LIMIT n` for pagination (not `FETCH FIRST n ROWS ONLY`). For offset: `LIMIT offset, count` or `LIMIT count OFFSET offset` -- Table names are case-sensitive on Linux, case-insensitive on macOS/Windows — always use exact case from the schema - -**Docs**: [MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/) diff --git a/plugins/shared/skills/resources_neo4j/SKILL.md b/plugins/shared/skills/resources_neo4j/SKILL.md deleted file mode 100644 index 4c28ec5..0000000 --- a/plugins/shared/skills/resources_neo4j/SKILL.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -name: using-neo4j-connector -description: Implements Neo4j Cypher queries, graph traversal, and node/relationship operations using generated clients and MCP tools. Use when doing ANYTHING that touches Neo4j or Cypher in any way, load this skill. ---- - -# Major Platform Resource: Neo4j - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__neo4j_query` — Execute read-only Cypher (MATCH/CALL/SHOW/WITH/RETURN only). Args: `resourceId`, `cypher`, `params?`, `maxResults?` -- `mcp__resources__neo4j_list_node_labels` — List all node labels. Args: `resourceId` -- `mcp__resources__neo4j_list_relationship_types` — List all relationship types. Args: `resourceId` -- `mcp__resources__neo4j_describe_schema` — Get constraints and indexes. Args: `resourceId` - -## TypeScript Client - -```typescript -import { graphDbClient } from "./clients"; - -// invoke(cypher, params?, invocationKey, timeoutMs?) -// Uses named parameters: $paramName -const result = await graphDbClient.invoke( - "MATCH (u:User {email: $email})-[:PURCHASED]->(p:Product) RETURN u, p LIMIT 10", - { email: "jane@example.com" }, - "fetch-user-purchases", -); -if (result.ok) { - const { records, keys } = result.result; - for (const record of records) { - console.log(record["u"].properties.name); - } -} -``` - -## Response Shape - -Graph types are converted to plain objects with a `_type` discriminator: - -| Neo4j Type | Shape | -| ------------ | ---------------------------------------------------------------------------------------- | -| Node | `{ _type: "node", _id, labels, properties }` | -| Relationship | `{ _type: "relationship", _id, _startNodeId, _endNodeId, relationshipType, properties }` | -| Path | `{ _type: "path", nodes[], relationships[] }` | - -## Tips - -- Uses **named parameters** (`$email`) not positional — pass `undefined` when no params needed -- MCP query tool is read-only; use the TypeScript client for CREATE/MERGE/DELETE operations -- Use `list_node_labels` and `list_relationship_types` to explore the graph structure before querying - -**Docs**: [Neo4j Cypher Manual](https://neo4j.com/docs/cypher-manual/current/) diff --git a/plugins/shared/skills/resources_notion/SKILL.md b/plugins/shared/skills/resources_notion/SKILL.md deleted file mode 100644 index 48a5d13..0000000 --- a/plugins/shared/skills/resources_notion/SKILL.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -name: using-notion-connector -description: Implements Notion API interactions for pages, databases, blocks, users, and search using generated clients and MCP tools. Use when doing ANYTHING that touches Notion workspaces, pages, or databases. ---- - -# Major Platform Resource: Notion - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Notion:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Notion API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__notion_search` — Search pages and databases in the Notion workspace. Args: `resourceId`, `query`, `filter?`, `sort?`, `startCursor?`, `pageSize?` -- `mcp__resources__notion_get_page` — Get a Notion page by ID. Args: `resourceId`, `pageId` -- `mcp__resources__notion_query_database` — Query a Notion database with optional filters and sorts. Args: `resourceId`, `databaseId`, `filter?`, `sorts?`, `startCursor?`, `pageSize?` -- `mcp__resources__notion_get_database` — Get a Notion database schema and properties. Args: `resourceId`, `databaseId` -- `mcp__resources__notion_get_block_children` — Get child blocks of a page or block. Args: `resourceId`, `blockId`, `startCursor?`, `pageSize?` -- `mcp__resources__notion_invoke` — Make any HTTP request to the Notion API. Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { notionClient } from "./clients"; - -// Generic invoke for any Notion API endpoint -const result = await notionClient.invoke("GET", "/v1/pages/PAGE_ID", "get-page"); -if (result.ok && result.result.status === 200 && result.result.body.kind === "json") { - const page = result.result.body.value; -} - -// POST with body -const searchResult = await notionClient.invoke("POST", "/v1/search", "search-pages", { - body: { type: "json", value: { query: "Meeting notes" } }, -}); - -// Query a database with filters -const queryResult = await notionClient.invoke("POST", "/v1/databases/DB_ID/query", "query-tasks", { - body: { - type: "json", - value: { - filter: { property: "Status", select: { equals: "Done" } }, - sorts: [{ property: "Created", direction: "descending" }], - }, - }, -}); -``` - -## Tips - -- All Notion API requests automatically include the `Notion-Version: 2022-06-28` header -- Notion uses UUIDs for page/database/block IDs (with or without hyphens) -- Pagination uses `start_cursor` and `has_more` pattern -- Database queries use a filter object — refer to Notion API docs for filter syntax -- Rich text is returned as arrays of rich text objects, not plain strings -- **Rate limit**: ~3 requests/second per integration. Handle HTTP 429 with Retry-After header. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` - -**Docs**: [Notion API Reference](https://developers.notion.com/reference) diff --git a/plugins/shared/skills/resources_outreach/SKILL.md b/plugins/shared/skills/resources_outreach/SKILL.md deleted file mode 100644 index 1418490..0000000 --- a/plugins/shared/skills/resources_outreach/SKILL.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: using-outreach-connector -description: Implements Outreach prospect and sequence management using generated clients and MCP tools. Use when doing ANYTHING that touches Outreach or Outreach.io in any way, load this skill. ---- - -# Major Platform Resource: Outreach - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Outreach:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Outreach API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__outreach_get` — Make a GET request to any Outreach API endpoint. Args: `resourceId`, `path`, `queryParams?` -- `mcp__resources__outreach_list_prospects` — List prospects with pagination. Args: `resourceId`, `limit?` -- `mcp__resources__outreach_list_sequences` — List sequences with pagination. Args: `resourceId`, `limit?` - -## TypeScript Client - -```typescript -import { outreachClient } from "./clients"; - -// invoke(method, path, invocationKey, options?) -const result = await outreachClient.invoke("GET", "/api/v2/prospects", "list-prospects", { - queryParams: { "page[limit]": "10" }, -}); -if (result.ok) { - console.log(result.result.data); -} - -// Create a prospect — uses JSON:API format -await outreachClient.invoke("POST", "/api/v2/prospects", "create-prospect", { - body: { - data: { - type: "prospect", - attributes: { firstName: "John", lastName: "Doe", emails: ["john@example.com"] }, - }, - }, -}); -``` - -## Tips - -- **Request bodies use JSON:API format**: `{ data: { type: "...", attributes: {...} } }` -- Pagination uses `page[limit]` and `page[offset]` query parameters -- Paths include the full API path: `/api/v2/prospects`, `/api/v2/sequences`, etc. - -**Docs**: [Outreach API Reference](https://developers.outreach.io/api/reference/) diff --git a/plugins/shared/skills/resources_postgresql/SKILL.md b/plugins/shared/skills/resources_postgresql/SKILL.md deleted file mode 100644 index 91086fc..0000000 --- a/plugins/shared/skills/resources_postgresql/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: using-postgresql-connector -description: Implements PostgreSQL connections, SQL queries, and migration patterns using generated clients and MCP tools. Use when doing ANYTHING that touches PostgreSQL, Postgres, pg, or psql in any way, load this skill. ---- - -# Major Platform Resource: PostgreSQL - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__postgresql_psql` — Execute read-only SQL queries and psql backslash commands (`\dt`, `\d`, `\di`, `\df`, etc.). Args: `resourceId`, `command`, `timeoutMs?` -- `mcp__resources__postgresql_invoke` — Execute write SQL against managed or external PostgreSQL resources. Args: `resourceId`, `sql`, `params?`, `timeoutMs?`, `description` - -## TypeScript Client - -```typescript -import { myDbClient } from "./clients"; - -// invoke(sql, params?, invocationKey, timeoutMs?) -const result = await myDbClient.invoke<{ id: number; name: string }>( - "SELECT * FROM users WHERE id = $1", - [userId], - "fetch-user", -); -if (result.ok) { - console.log(result.result.rows); -} -``` - -## Tips - -- Use parameterized queries (`$1`, `$2`, ...) — never interpolate values into SQL strings -- `psql` is read-only; use `postgresql_invoke` for data modifications against managed or external PostgreSQL resources -- The TypeScript client supports full read/write operations regardless of managed status -- Use `psql` exclusively for read-only tasks. Never use invoke for read only. - -**Docs**: [PostgreSQL Documentation](https://www.postgresql.org/docs/) diff --git a/plugins/shared/skills/resources_quickbooks/SKILL.md b/plugins/shared/skills/resources_quickbooks/SKILL.md deleted file mode 100644 index 9b22979..0000000 --- a/plugins/shared/skills/resources_quickbooks/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: using-quickbooks-connector -description: Implements QuickBooks Online accounting data access for customers, invoices, items, accounts, vendors, bills, and payments using generated clients and MCP tools. Use when doing ANYTHING that touches QuickBooks in any way, load this skill. ---- - -# Major Platform Resource: QuickBooks Online - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__quickbooks_query` — Execute a QuickBooks query using SQL-like syntax. Args: `resourceId`, `query`, `timeoutMs?` -- `mcp__resources__quickbooks_get` — Get a specific entity by type and ID. Args: `resourceId`, `entityType`, `entityId` -- `mcp__resources__quickbooks_invoke` — Make any HTTP request to the QuickBooks API, including write operations. Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { qbClient } from "./clients"; - -// Query entities using SQL-like syntax -const result = await qbClient.query("SELECT * FROM Customer WHERE DisplayName LIKE 'A%'", "search-customers"); -if (result.ok && result.result.body.kind === "json") { - const data = result.result.body.value; -} - -// Generic invoke for any operation -const invoice = await qbClient.invoke("GET", "/invoice/123", "get-invoice"); - -// Create an invoice -const newInvoice = await qbClient.invoke("POST", "/invoice", "create-invoice", { - body: { - type: "json", - value: { - Line: [{ Amount: 100.0, DetailType: "SalesItemLineDetail" }], - CustomerRef: { value: "1" }, - }, - }, -}); -``` - -## Tips - -- **QuickBooks Query Language** is SQL-like but has limitations: - - No JOINs, no OR in WHERE clauses, no GROUP BY - - Use `LIKE` with `%` for wildcard matching - - Only filterable properties can be used in WHERE clauses - - Example: `SELECT * FROM Invoice WHERE TotalAmt > '100.00'` -- **Common entity types**: Customer, Invoice, Item, Account, Vendor, Bill, Payment, Estimate, PurchaseOrder, SalesReceipt, CreditMemo, Employee -- **All API paths are relative** to `/v3/company/{realmId}` — the realmId is handled automatically -- **Rate limit**: 500 requests per minute per realm. Respect throttling headers. -- Response structure: `{ kind: "api", status: number, body: { kind: "json", value: {...} } }` -- **Updates require SyncToken**: When updating entities, include the current `SyncToken` from the entity to prevent conflicts - -**Docs**: [QuickBooks Online API Reference](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account) diff --git a/plugins/shared/skills/resources_ringcentral/SKILL.md b/plugins/shared/skills/resources_ringcentral/SKILL.md deleted file mode 100644 index 2853831..0000000 --- a/plugins/shared/skills/resources_ringcentral/SKILL.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: using-ringcentral-connector -description: Implements RingCentral API access (call logs, messages, SMS, extensions) through the Major HTTP proxy. Use when doing ANYTHING that touches RingCentral in any way, load this skill. ---- - -# Major Platform Resource: RingCentral - -RingCentral is a proxy-only resource — every call goes through Major's HTTP proxy. There is **no typed `ringcentral_*` MCP tool and no generated TypeScript client**. The proxy resolves the tenant host (`.ringcentral.com`) at request time from the connected account, so you never hardcode or pass the instance host — always pass a **leading-slash relative path** like `/restapi/v1.0/account/~/call-log`. - -**Security**: Never set the `Authorization` header — the proxy injects it. Reserved headers (`Authorization`, `Cookie`, `Host`, `Forwarded`, `X-Forwarded-*`, `X-Real-Ip`, `X-Major-*`) are stripped on the way out. - ---- - -## MCP Tools - -Use the generic HTTP proxy tools — there are no RingCentral-specific MCP tools. - -- `mcp__resources__http_proxy_get` — Read-only GET. Args: `resourceId`, `url` (leading-slash path), `headers?`, `timeoutMs?` -- `mcp__resources__http_proxy_invoke` — Any HTTP method. Args: `resourceId`, `method`, `url` (leading-slash path), `headers?`, `body?`, `timeoutMs?` - -See [using-http-proxy](../http-proxy/SKILL.md) for the full proxy reference. - -## HTTP Proxy via `createProxyFetch` (Next.js) - -For app code, drop the generic `createProxyFetch` into any SDK that takes a custom `fetch`, or use it as a plain `fetch` wrapper. Pass **leading-slash paths** instead of a full URL — the proxy resolves the upstream host. - -```typescript -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const proxyFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.RINGCENTRAL_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -// List recent call log entries -const res = await proxyFetch("/restapi/v1.0/account/~/call-log?perPage=25&dateFrom=2024-01-01T00:00:00Z"); -if (!res.ok) throw new Error(`RingCentral ${res.status}: ${await res.text()}`); -const { records } = await res.json(); - -// List extensions -const ext = await proxyFetch("/restapi/v1.0/account/~/extension?perPage=100"); - -// Send an SMS -const sms = await proxyFetch("/restapi/v1.0/account/~/extension/~/sms", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - from: { phoneNumber: "+15551234567" }, - to: [{ phoneNumber: "+15559876543" }], - text: "Hello from Major!", - }), -}); - -// List messages from the message store -await proxyFetch("/restapi/v1.0/account/~/extension/~/message-store?messageType=SMS&perPage=25"); -``` - -**Rules:** - -- `MAJOR_API_BASE_URL` and `MAJOR_JWT_TOKEN` are platform-managed env vars — assume they're set, don't ask the user to provide them. `RINGCENTRAL_RESOURCE_ID` is the UUID of the connected RingCentral resource. -- **Always pass a leading-slash path.** Hardcoding `https://.ringcentral.com/...` will fail — the instance host isn't known to app code. -- **`resourceId` must be a static string literal** — the query extractor only tracks proxy calls when the resource id is known at build time. -- Server-side only (Server Components, Route Handlers, Server Actions). The `/next` subpath uses `next/headers`. -- Do NOT set the `Authorization` header — the proxy injects upstream auth. - -## Common RingCentral endpoints - -| What | Method | Path | -| -------------------- | ------ | --------------------------------------------------------------------- | -| Current extension | GET | `/restapi/v1.0/account/~/extension/~` | -| List extensions | GET | `/restapi/v1.0/account/~/extension` | -| Get one extension | GET | `/restapi/v1.0/account/~/extension/{extensionId}` | -| List call log | GET | `/restapi/v1.0/account/~/call-log` | -| Get one call record | GET | `/restapi/v1.0/account/~/call-log/{callRecordId}` | -| List messages | GET | `/restapi/v1.0/account/~/extension/~/message-store` | -| Send SMS | POST | `/restapi/v1.0/account/~/extension/~/sms` | - -The `~` token means "the current account / authenticated extension" — RingCentral resolves it from the connected credential, so you don't substitute an id. - -## Tips - -- **`~` is RingCentral's self-reference.** Use `account/~` for the connected account and `extension/~` for the authenticated extension; pass a concrete id only when targeting a different extension. -- **Call log & message filters are query params**: `dateFrom`, `dateTo`, `direction` (`Inbound`/`Outbound`), `type` (`Voice`/`Fax`), `messageType` (`SMS`/`Fax`/`VoiceMail`/`Pager`), `perPage`, `page`. -- **SMS bodies are structured**: `{ from: { phoneNumber }, to: [{ phoneNumber }], text }`. Numbers must be E.164 (`+1...`). -- **Pagination** is page-based: `perPage` + `page`; responses carry `paging` and `navigation` blocks. -- **Non-2xx upstream statuses are passed through** — always inspect `res.ok` / `res.status` before treating the body as success. -- **Rate limits**: RingCentral groups endpoints into Light/Medium/Heavy buckets with per-minute limits; expect `429` with a `Retry-After` header under load. - -**Docs**: [RingCentral REST API Reference](https://developers.ringcentral.com/api-reference) diff --git a/plugins/shared/skills/resources_s3/SKILL.md b/plugins/shared/skills/resources_s3/SKILL.md deleted file mode 100644 index 50d6b02..0000000 --- a/plugins/shared/skills/resources_s3/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: using-s3-connector -description: Implements Amazon S3 object operations, presigned URLs, and file uploads/downloads using generated clients and MCP tools. Use when doing ANYTHING that touches S3 or AWS S3 in any way, load this skill. ---- - -# Major Platform Resource: Amazon S3 - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__s3_list_buckets` — List all accessible buckets. Args: `resourceId` -- `mcp__resources__s3_list_objects` — List objects with optional prefix/delimiter. Args: `resourceId`, `bucket`, `prefix?`, `delimiter?`, `maxKeys?` -- `mcp__resources__s3_get_object_metadata` — Get size, content type, last modified. Args: `resourceId`, `bucket`, `key` - -## TypeScript Client - -```typescript -import { storageClient } from "./clients"; - -// Generate presigned URL for upload -const uploadResult = await storageClient.invoke( - { command: "PutObject", key: "uploads/image.jpg", presignedUrl: true, expiresIn: 3600 }, - "generate-upload-url", -); -if (uploadResult.ok) { - const url = uploadResult.result.presignedUrl; - // Return URL to frontend for direct upload -} - -// List objects -const listResult = await storageClient.invoke({ command: "ListObjectsV2", prefix: "uploads/" }, "list-uploads"); -``` - -## Tips - -- **Always use presigned URLs** for uploads and downloads — generate on server, return URL to frontend for direct S3 access -- **Never proxy file contents** through your application server -- S3 commands: `PutObject`, `GetObject`, `ListObjectsV2`, `DeleteObject`, `HeadObject` -- Compatible with AWS S3, MinIO, DigitalOcean Spaces, and other S3-compatible storage - -**Docs**: [Amazon S3 Documentation](https://docs.aws.amazon.com/s3/) diff --git a/plugins/shared/skills/resources_salesforce/SKILL.md b/plugins/shared/skills/resources_salesforce/SKILL.md deleted file mode 100644 index c0424da..0000000 --- a/plugins/shared/skills/resources_salesforce/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: using-salesforce-connector -description: Implements Salesforce SOQL queries, sObject CRUD, and metadata exploration using generated clients and MCP tools. Use when doing ANYTHING that touches Salesforce, SFDC, or SOQL in any way, load this skill. ---- - -# Major Platform Resource: Salesforce - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Salesforce:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Salesforce API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__salesforce_get` — Make a GET request to any Salesforce API endpoint. Args: `resourceId`, `path`, `queryParams?` -- `mcp__resources__salesforce_query` — Execute a SOQL query. Args: `resourceId`, `query` -- `mcp__resources__salesforce_describe_object` — Get metadata and field definitions for an sObject. Args: `resourceId`, `objectType` - -## TypeScript Client - -```typescript -import { sfClient } from "./clients"; - -// Prefer helper methods over raw invoke() -const result = await sfClient.query( - "SELECT Id, Name FROM Account WHERE CreatedDate > 2024-01-01T00:00:00Z LIMIT 10", - "recent-accounts", -); - -// CRUD helpers -await sfClient.getRecord("Account", recordId, "get-account", { fields: ["Name", "Industry"] }); -await sfClient.createRecord("Account", { Name: "Acme Corp" }, "create-account"); -await sfClient.updateRecord("Account", recordId, { Name: "Updated" }, "update-account"); -await sfClient.deleteRecord("Account", recordId, "delete-account"); -await sfClient.describeObject("Account", "describe-account"); -``` - -## Tips - -- **Use `query()` helper for SOQL** — cleaner than building the path manually -- **Governor limits**: Be mindful of API call limits (varies by org edition). Use bulk API for large data operations. -- Use `describeObject()` to explore field names and types before writing queries -- Salesforce API paths include the version: `/services/data/v63.0/...` - -**Docs**: [Salesforce REST API Reference](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) diff --git a/plugins/shared/skills/resources_sharepoint/SKILL.md b/plugins/shared/skills/resources_sharepoint/SKILL.md deleted file mode 100644 index 4582ad7..0000000 --- a/plugins/shared/skills/resources_sharepoint/SKILL.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -name: using-sharepoint-connector -description: Implements Microsoft SharePoint access — sites, lists, document libraries, and file operations — using generated clients and MCP tools. Use when doing ANYTHING that touches SharePoint, OneDrive for Business, or Microsoft Graph Sites/Files API. ---- - -# Major Platform Resource: SharePoint - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** Always read the actual client source code to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only. Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.json`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-team-sites"`, never dynamic values. - ---- - -## MCP Tools - -- `mcp__resources__sharepoint_list_sites` — List SharePoint sites accessible to the connected account. Args: `resourceId`, `search?`, `top?`, `select?` -- `mcp__resources__sharepoint_get_site_items` — Get items from a SharePoint list. Args: `resourceId`, `siteId`, `listId`, `select?`, `filter?`, `expand?`, `top?` -- `mcp__resources__sharepoint_search_drive_items` — Search for files across SharePoint drives. Args: `resourceId`, `query`, `top?` -- `mcp__resources__sharepoint_get_file_download_url` — Get a pre-authenticated download URL for a file. Args: `resourceId`, `siteId`, `itemId` -- `mcp__resources__sharepoint_create_upload_session` — Create a pre-authenticated upload session. Args: `resourceId`, `siteId`, `fileName`, `parentPath?`, `conflictBehavior?` -- `mcp__resources__sharepoint_get` — Generic GET request to any Microsoft Graph endpoint. Args: `resourceId`, `path`, `query?` -- `mcp__resources__sharepoint_invoke` — Generic HTTP request to Microsoft Graph (for JSON write operations). Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -## TypeScript Client - -```typescript -import { spClient } from "./clients"; - -// List sites -const sites = await spClient.invoke("GET", "/v1.0/sites?search=*", "list-all-sites"); - -// Get items from a SharePoint list -const items = await spClient.invoke( - "GET", - "/v1.0/sites/{site-id}/lists/{list-id}/items?$expand=fields", - "get-list-items" -); - -// Search for files -const files = await spClient.invoke( - "GET", - "/v1.0/sites/{site-id}/drive/root/search(q='quarterly report')", - "search-files" -); - -// Create a list item (body is just a plain object — no wrapper needed) -const newItem = await spClient.invoke( - "POST", - "/v1.0/sites/{site-id}/lists/{list-id}/items", - "create-item", - { body: { fields: { Title: "New Item", Status: "Active" } } } -); -``` - -## File Downloads and Uploads - -SharePoint files must be downloaded and uploaded using **pre-authenticated URLs** (presigned URLs), not through the connector's invoke method. The connector brokers the authenticated handshake with Microsoft Graph to obtain these URLs, but the actual binary transfer goes directly between your app and Microsoft's servers. - -### Downloading a file - -Use `get_file_download_url` (MCP) or request the `@microsoft.graph.downloadUrl` property (client) to get a short-lived presigned URL, then fetch the file directly from that URL. - -**Via MCP tool:** -``` -mcp__resources__sharepoint_get_file_download_url({ resourceId, siteId, itemId }) -→ { id, name, size, "@microsoft.graph.downloadUrl": "https://..." } -``` - -**Via TypeScript client:** -```typescript -import { spClient } from "./clients"; - -// Step 1: Get the presigned download URL through the connector -const result = await spClient.invoke( - "GET", - "/v1.0/sites/{site-id}/drive/items/{item-id}?select=id,name,size,@microsoft.graph.downloadUrl", - "get-download-url" -); -if (!result.ok) throw new Error(result.error.message); - -const downloadUrl = result.json["@microsoft.graph.downloadUrl"]; - -// Step 2: Download the file directly — no auth headers needed -const fileResponse = await fetch(downloadUrl); -const fileBuffer = await fileResponse.arrayBuffer(); -``` - -### Uploading a file - -Use `create_upload_session` (MCP) or POST to `createUploadSession` (client) to get a presigned upload URL, then PUT file bytes directly to that URL. - -**Via MCP tool:** -``` -mcp__resources__sharepoint_create_upload_session({ resourceId, siteId, fileName: "report.pdf", parentPath: "Documents/Reports" }) -→ { uploadUrl: "https://...", expirationDateTime: "..." } -``` - -**Via TypeScript client:** -```typescript -import { spClient } from "./clients"; -import fs from "fs"; - -// Step 1: Create an upload session through the connector -const session = await spClient.invoke( - "POST", - "/v1.0/sites/{site-id}/drive/root:/Documents/report.pdf:/createUploadSession", - "create-upload-session", - { - body: { - item: { - "@microsoft.graph.conflictBehavior": "rename", - name: "report.pdf", - }, - }, - } -); -if (!session.ok) throw new Error(session.error.message); - -const uploadUrl = session.json.uploadUrl; - -// Step 2: PUT the file bytes directly — no auth headers needed -const fileBuffer = fs.readFileSync("./report.pdf"); -const uploadResponse = await fetch(uploadUrl, { - method: "PUT", - headers: { - "Content-Length": String(fileBuffer.byteLength), - "Content-Range": `bytes 0-${fileBuffer.byteLength - 1}/${fileBuffer.byteLength}`, - }, - body: fileBuffer, -}); -const uploaded = await uploadResponse.json(); // returns the driveItem -``` - -For files larger than 4MB, split into ~10MB chunks and PUT each with the appropriate `Content-Range` header. The upload session URL handles ordering and resumability automatically. - -## Tips - -- **Microsoft Graph API**: All paths are relative to `https://graph.microsoft.com`. Use `/v1.0/` prefix for stable endpoints. -- **File operations**: Always use the presigned URL tools (`get_file_download_url`, `create_upload_session`) for binary file transfer. The generic `invoke` tool only handles JSON request/response bodies. -- **Admin consent**: These scopes use delegated permissions and do NOT require admin consent by default. However, some Microsoft 365 tenants disable user consent org-wide — in that case, a tenant admin will need to approve the app once. -- **Common SharePoint paths**: - - Sites: `/v1.0/sites?search=keyword`, `/v1.0/sites/{hostname}:/{server-relative-path}` - - Lists: `/v1.0/sites/{site-id}/lists`, `/v1.0/sites/{site-id}/lists/{list-id}/items` - - Drives: `/v1.0/sites/{site-id}/drives`, `/v1.0/sites/{site-id}/drive/root/children` - - Files: `/v1.0/sites/{site-id}/drive/items/{item-id}`, `/v1.0/sites/{site-id}/drive/root:/{path}` -- **OData queries**: Use `$select`, `$filter`, `$expand`, `$top`, `$orderby` as query parameters -- **Pagination**: Graph API uses `@odata.nextLink` for pagination — pass the full URL to `invoke` for subsequent pages - -**Docs**: [Microsoft Graph SharePoint API Reference](https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0) diff --git a/plugins/shared/skills/resources_slack/SKILL.md b/plugins/shared/skills/resources_slack/SKILL.md deleted file mode 100644 index 24820da..0000000 --- a/plugins/shared/skills/resources_slack/SKILL.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -name: using-slack-connector -description: Implements Slack messaging, channel operations, and Web API calls using generated clients and MCP tools. Use when doing ANYTHING that touches Slack in any way, load this skill. ---- - -# Major Platform Resource: Slack - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Slack:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the Slack API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## CRITICAL: Channel Access Verification - -Before sending messages, posting files, or reading history from any channel, you MUST verify the bot has access to that channel. **Never attempt to post to a channel without confirming access first.** - -**Required workflow:** - -1. Call `mcp__resources__slack_list_channels` to get the list of channels the bot can see. -2. Check if the target channel appears in the results. -3. **If the channel is NOT in the list:** Tell the user that the bot does not currently have access to that channel. Ask them to invite the bot by going to the channel and @mentioning **@Major Slack Integration**. Once the user confirms they have done this, call `mcp__resources__slack_list_channels` again to verify the channel now appears. -4. **Only after the channel is confirmed visible** in the list may you proceed with sending messages, reading history, or any other channel operation. - -**Do NOT skip this check.** Do NOT assume the bot has access to a channel just because the user mentioned it by name. - ---- - -## CRITICAL: Rate Limits - -Slack's Web API rate limits are aggressive and easy to blow through, especially on history/read methods: - -- Rate limits are per-workspace, tiered by method, and enforced with `429` + a `Retry-After` header. Tiers range from Tier 1 (~1 request/minute) to Tier 4 (~100+ requests/minute), and non-Marketplace apps got hit hard by a 2026 change: `conversations.history` dropped from Tier 3 (~50 req/min, 100+ messages per call) to Tier 1 (1 req/min, max 15 messages per call) for non-Marketplace/custom apps. Assume you are on the low end unless you've confirmed otherwise. -- **Do not parallelize Slack queries.** Firing requests from multiple agents/workflows at once (e.g. one sub-agent per channel) multiplies 429s instead of avoiding them — all requests share the same workspace-level bucket. Query Slack serially, one call at a time. -- **When pulling a lot of message history, use the highest `limit` the read tool allows per call** (up to its max, e.g. 1000 if supported) instead of small page sizes. Fewer, larger calls burn far less of the rate-limit budget than many small ones — pulling 20 messages at a time across many pages is the single biggest way to exhaust the limit for no benefit. -- **If you keep hitting rate limit errors, stop and reassess instead of retrying the same way.** Repeatedly re-calling into a 429 (or spinning up more parallel agents to "get around it") burns time and money without getting more data — it just waits out the same shared limit slower. Back off, reduce concurrency to 1, and if you've already gathered a reasonable amount of history, answer the user's question with what you have rather than grinding to fetch everything. - ---- - -## MCP Tools - -- `mcp__resources__slack_call` — Call any Slack Web API method. Args: `resourceId`, `method`, `body?` -- `mcp__resources__slack_list_channels` — List channels in the workspace. Args: `resourceId`, `limit?` -- `mcp__resources__slack_post_message` — Post a message to a channel. Args: `resourceId`, `channel`, `text`, `blocks?` -- `mcp__resources__slack_get_history` — Get message history from a channel. Args: `resourceId`, `channel`, `limit?` - -## TypeScript Client - -```typescript -import { slackClient } from "./clients"; - -// invoke(method, invocationKey, options?) -// The `method` parameter is the Slack API method name -const result = await slackClient.invoke("chat.postMessage", "post-update", { - body: { channel: "C0123456", text: "Hello from the app!" }, -}); - -// List channels -await slackClient.invoke("conversations.list", "list-channels", { - body: { limit: 100 }, -}); - -// getUploadURL(filename, length, invocationKey, options?) -// completeUpload(files, channelId, invocationKey, options?) -// See "File Upload" section below for usage -``` - -## File Upload - -Slack uses a 3-step flow for uploading files/images to channels: - -```typescript -import { slackClient } from "./clients"; - -// Step 1: Get a pre-signed upload URL -const urlResult = await slackClient.getUploadURL( - "chart.png", // filename with extension - fileBytes.length, // file size in bytes - "get-upload-url", -); -if (!urlResult.ok) throw new Error(urlResult.error.message); -const { upload_url, file_id } = urlResult.result.body.value; - -// Step 2: Upload the file binary to the pre-signed URL -await fetch(upload_url, { - method: "POST", - headers: { "Content-Type": "application/octet-stream" }, - body: fileBytes, -}); - -// Step 3: Complete the upload and share to a channel -const completeResult = await slackClient.completeUpload( - [{ id: file_id, title: "Weekly Chart" }], - "C0123456", // channel ID - "complete-upload", - { initialComment: "Here's this week's chart" }, -); -``` - -- The upload URL from step 1 is temporary — complete all 3 steps without delay -- Step 2 is a direct HTTP POST (no auth needed, the URL is pre-signed) -- You can upload multiple files by calling step 1+2 for each, then passing all file IDs to a single step 3 -- Use `threadTs` in step 3 options to upload into a thread -- Requires `files:write` OAuth scope (included in the "Read & Write" preset) - -## Tips - -- The `method` param is the **Slack API method name** (e.g., `chat.postMessage`, `conversations.list`, `users.list`) -- For the TypeScript client, all parameters go in the `body` option — Slack's Web API uses POST with JSON body -- Check [Slack API methods list](https://api.slack.com/methods) for available methods and their parameters -- **If a message/history response is too large and gets written to a file instead of returned inline, read it with `jq` rather than loading the whole file** — e.g. `jq '.messages[] | {user, text, ts}' file.json` — to avoid pulling the entire payload into context. - -**Docs**: [Slack API Reference](https://api.slack.com/methods) diff --git a/plugins/shared/skills/resources_snowflake/SKILL.md b/plugins/shared/skills/resources_snowflake/SKILL.md deleted file mode 100644 index b2ed040..0000000 --- a/plugins/shared/skills/resources_snowflake/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: using-snowflake-connector -description: Implements Snowflake warehouse queries, schema exploration, and data operations using generated clients and MCP tools. Use when doing ANYTHING that touches Snowflake in any way, load this skill. ---- - -# Major Platform Resource: Snowflake - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** — use descriptive literals like `"fetch-user-orders"`, never dynamic values like `` `${date}-records` ``. - ---- - -## MCP Tools - -- `mcp__resources__snowflake_list_databases` — List all accessible databases. Args: `resourceId` -- `mcp__resources__snowflake_list_schemas` — List schemas in a database. Args: `resourceId`, `database` -- `mcp__resources__snowflake_list_tables` — List tables in a schema. Args: `resourceId`, `database`, `schema` -- `mcp__resources__snowflake_query` — Execute read-only SQL (SELECT/SHOW/DESCRIBE/EXPLAIN). Args: `resourceId`, `statement`, `database?`, `schema?` - -## TypeScript Client - -```typescript -import { snowflakeClient } from "./clients"; - -// execute(statement, invocationKey, options?) -const result = await snowflakeClient.execute( - "SELECT * FROM orders WHERE order_date > '2024-01-01' LIMIT 100", - "recent-orders", - { database: "ANALYTICS", schema: "PUBLIC" }, -); - -// status(statementHandle, invocationKey, options?) — for async queries -// cancel(statementHandle, invocationKey) -``` - -## Tips - -- MCP tools are **all read-only** — use the TypeScript client for write operations -- Use `list_databases` → `list_schemas` → `list_tables` to explore data warehouse structure -- The `query` tool accepts optional `database` and `schema` context parameters -- For long-running queries via the TypeScript client, use async execution with `status()` polling - -**Docs**: [Snowflake Documentation](https://docs.snowflake.com/) diff --git a/plugins/shared/skills/resources_sqs/SKILL.md b/plugins/shared/skills/resources_sqs/SKILL.md deleted file mode 100644 index fb344ae..0000000 --- a/plugins/shared/skills/resources_sqs/SKILL.md +++ /dev/null @@ -1,93 +0,0 @@ ---- - -name: using-sqs-connector - -description: Implements AWS SQS message queue operations for sending, receiving, and managing messages using generated clients and MCP tools. Use when doing ANYTHING that touches SQS in any way, load this skill. - ---- - -# Major Platform Resource: AWS SQS - -## Common: Interacting with Resources - -**Security**: Never connect directly to databases/APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Two ways to interact with resources:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. - -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** -- use descriptive literals like `"send-order-notification"`, never dynamic values like `` `${queueName}-send` ``. - ---- - -## MCP Tools - -- `mcp__resources__sqs_list_queues` -- List all SQS queues. Args: `resourceId`, `queueNamePrefix?`, `maxResults?` - -- `mcp__resources__sqs_get_queue_attributes` -- Get queue attributes (message count, delay, etc). Args: `resourceId`, `queueUrl` - -- `mcp__resources__sqs_send_message` -- Send a message to a queue. Args: `resourceId`, `queueUrl`, `messageBody`, `delaySeconds?`, `messageGroupId?`, `messageDeduplicationId?` - -- `mcp__resources__sqs_receive_message` -- Receive messages from a queue. Args: `resourceId`, `queueUrl`, `maxNumberOfMessages?`, `waitTimeSeconds?`, `visibilityTimeout?` - -- `mcp__resources__sqs_delete_message` -- Delete a processed message. Args: `resourceId`, `queueUrl`, `receiptHandle` - -- `mcp__resources__sqs_invoke` -- Generic command execution. Args: `resourceId`, `command`, `queueUrl?`, `params?` - -## TypeScript Client - -```typescript - -import { sqsClient } from "./clients"; - -// invoke(command, params, invocationKey, options?) - -const result = await sqsClient.invoke( - - "SendMessage", - - { - - queueUrl: "[https://sqs.us-east-1.amazonaws.com/123456789012/my-queue](https://sqs.us-east-1.amazonaws.com/123456789012/my-queue)", - - messageBody: JSON.stringify({ orderId: "12345" }), - - }, - - "send-order-notification", - -); - -if (result.ok) { - - console.log("Message ID:", [result.result.data](http://result.result.data).MessageId); - -} - -``` - -## Tips - -- **Queue URLs, not names**: Most SQS operations require the full queue URL, not just the queue name. Use `sqs_list_queues` first to get URLs. - -- **Receipt handles for deletion**: After receiving a message, use the `receiptHandle` from the response to delete it. Receipt handles expire after the visibility timeout. - -- **FIFO queues**: If the queue URL ends in `.fifo`, you must provide `messageGroupId` and `messageDeduplicationId` when sending messages. - -- **Visibility timeout**: When you receive a message, it becomes invisible to other consumers for the visibility timeout period. Process and delete it within this window, or it will reappear. - -- **Long polling**: Set `waitTimeSeconds` (up to 20) on receive to reduce empty responses and API costs. - -- **Message size limit**: SQS messages can be up to 256 KB. For larger payloads, store the data in S3 and send a reference. - -- **Batch operations**: Use `sqs_invoke` with `DeleteMessageBatch` command to delete up to 10 messages at once. - -- **PurgeQueue is not available**: For safety, bulk queue purging is not exposed through the platform. Delete messages individually or in batches instead. \ No newline at end of file diff --git a/plugins/shared/skills/resources_stripe/SKILL.md b/plugins/shared/skills/resources_stripe/SKILL.md deleted file mode 100644 index 944ecba..0000000 --- a/plugins/shared/skills/resources_stripe/SKILL.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -name: using-stripe-connector -description: Implements Stripe payment API access for customers, payments, subscriptions, invoices, and balance using generated clients and MCP tools. Use when doing ANYTHING that touches Stripe in any way, load this skill. ---- - -# Major Platform Resource: Stripe - -## Common: Interacting with Resources - -**Security**: Never connect directly to APIs. Never use credentials in code. Always use generated clients or MCP tools. - -**Three ways to interact with Stripe:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources___`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **Official Stripe SDK via the HTTP proxy** (Next.js apps): Pass `createProxyFetch` into `Stripe.createFetchHttpClient(...)`. See the **Stripe SDK via the HTTP proxy** section below — preferred when you want full Stripe SDK ergonomics (typed methods, autocomplete, automatic pagination). - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and their exact signatures before writing any client code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `response.ok` before accessing `response.json`. - -**Invocation keys must be static strings** — use descriptive literals like `"list-customers"`, never dynamic values like `` `${date}-customers` ``. - ---- - -## MCP Tools - -**Tool selection:** For any read-only operation, prefer `stripe_get` (or a specialized `stripe_list_*` / `stripe_get_*` tool) over `stripe_invoke`. Reserve `stripe_invoke` for writes (POST/PUT/DELETE) or endpoints the other tools don't cover. - -- `mcp__resources__stripe_get` — **Preferred for all read-only (GET) requests.** Make a GET request to any Stripe API endpoint. Args: `resourceId`, `path`, `query?` -- `mcp__resources__stripe_list_customers` — List customers with optional email filter and cursor pagination. Args: `resourceId`, `email?`, `limit?`, `startingAfter?` -- `mcp__resources__stripe_get_customer` — Get a single customer by ID. Args: `resourceId`, `customerId` -- `mcp__resources__stripe_list_payment_intents` — List payment intents with optional filters. Args: `resourceId`, `customer?`, `status?`, `limit?`, `startingAfter?` -- `mcp__resources__stripe_get_balance` — Get the current account balance. Args: `resourceId` -- `mcp__resources__stripe_list_subscriptions` — List subscriptions with optional filters. Args: `resourceId`, `customer?`, `status?`, `limit?`, `startingAfter?` -- `mcp__resources__stripe_list_invoices` — List invoices with optional filters. Args: `resourceId`, `customer?`, `status?`, `limit?`, `startingAfter?` -- `mcp__resources__stripe_invoke` — Make any HTTP request (GET/POST/PUT/DELETE). **Use only for write operations** (POST/PUT/DELETE) or endpoints not covered by `stripe_get` / `stripe_list_*` / `stripe_get_*`. Args: `resourceId`, `method`, `path`, `query?`, `headers?`, `body?`, `timeoutMs?` - -## TypeScript Client - -The client exposes a single `invoke(method, path, invocationKey, options?)` method. The generic `T` types the parsed JSON response, available directly on `.json`. - -### Reading data - -```typescript -import { stripeClient } from "./clients"; - -// List customers filtered by email -const response = await stripeClient.invoke<{ - data: Array<{ id: string; email: string; name: string }>; - has_more: boolean; -}>("GET", "/v1/customers", "list-customers", { - query: { limit: ["10"], email: ["jane@example.com"] }, -}); - -if (response.ok) { - for (const c of response.json.data) { - console.log(c.id, c.email); - } -} - -// Get account balance -const balance = await stripeClient.invoke<{ - available: Array<{ amount: number; currency: string }>; - pending: Array<{ amount: number; currency: string }>; -}>("GET", "/v1/balance", "get-balance"); - -if (balance.ok) { - console.log("Available:", balance.json.available); -} - -// Get a single subscription -const sub = await stripeClient.invoke<{ - id: string; - status: string; - current_period_end: number; -}>("GET", "/v1/subscriptions/sub_abc123", "get-subscription"); - -if (sub.ok) { - console.log(`Status: ${sub.json.status}`); -} -``` - -### Writing data - -Stripe's v1 API expects `application/x-www-form-urlencoded` bodies for write operations. Use `type: "form"` with a **flat** object whose keys match the Stripe docs. For nested parameters, use Stripe's bracket notation in the key name (e.g. `"metadata[order_id]"`). Do NOT use nested objects or arrays — the platform rejects them. Values must be primitives (string, number, boolean) or null (omitted). - -```typescript -// Create a customer — form-encoded body -const created = await stripeClient.invoke<{ id: string }>( - "POST", "/v1/customers", "create-customer", - { - body: { - type: "form", - value: { - email: "jane@example.com", - name: "Jane Doe", - "metadata[source]": "onboarding", - }, - }, - }, -); - -if (created.ok) { - console.log("Created:", created.json.id); -} - -// Create a payment intent with nested params via bracket keys -const payment = await stripeClient.invoke<{ id: string; client_secret: string }>( - "POST", "/v1/payment_intents", "create-payment-intent", - { - body: { - type: "form", - value: { - amount: 2000, - currency: "usd", - customer: "cus_abc123", - "automatic_payment_methods[enabled]": true, - }, - }, - }, -); - -if (payment.ok) { - console.log("Client secret:", payment.json.client_secret); -} - -// Cancel a subscription -await stripeClient.invoke("DELETE", "/v1/subscriptions/sub_xyz789", "cancel-subscription"); - -// Create a customer session using a preview API version with idempotency -const session = await stripeClient.invoke<{ client_secret: string }>( - "POST", "/v1/customer_sessions", "create-customer-session", - { - headers: { - "Stripe-Version": "2026-03-25.preview", - "Idempotency-Key": "session-abc-123", - }, - body: { - type: "form", - value: { - customer: "cus_abc123", - "components[pricing_table][enabled]": true, - }, - }, - }, -); -``` - -### Pagination - -```typescript -interface Invoice { id: string; amount_due: number; status: string } - -let hasMore = true; -let startingAfter: string | undefined; -const allInvoices: Invoice[] = []; - -while (hasMore) { - const query: Record = { limit: ["100"], status: ["paid"] }; - if (startingAfter) { - query.starting_after = [startingAfter]; - } - - const page = await stripeClient.invoke<{ data: Invoice[]; has_more: boolean }>( - "GET", "/v1/invoices", "list-invoices", { query }, - ); - - if (page.ok) { - allInvoices.push(...page.json.data); - hasMore = page.json.has_more; - if (page.json.data.length > 0) { - startingAfter = page.json.data[page.json.data.length - 1].id; - } - } else { - break; - } -} -``` - -## Headers - -Pass custom HTTP headers via the `headers` option. Common Stripe headers: - -| Header | Purpose | -|--------|---------| -| `Stripe-Version` | Pin a specific API version (e.g. `"2026-03-25.preview"` for preview features). | -| `Idempotency-Key` | Ensure POST requests are idempotent — Stripe deduplicates by this key. | -| `Stripe-Account` | Make requests on behalf of a connected account (Stripe Connect). | - -**Protected headers:** `Authorization` and `Content-Type` are managed by the connector and **cannot** be set via `headers`. Attempting to do so returns an error. - -```typescript -await stripeClient.invoke("GET", "/v1/balance", "get-connected-balance", { - headers: { "Stripe-Account": "acct_connected123" }, -}); -``` - -## Body Types - -The `body` option supports multiple types: - -| Type | Content-Type | When to use | -|------|-------------|-------------| -| `"form"` | `application/x-www-form-urlencoded` | **Default for Stripe writes.** All v1 POST/PUT/PATCH endpoints expect form encoding. Use flat keys with bracket notation for nested params. | -| `"json"` | `application/json` | v2 API endpoints that accept JSON. | -| `"text"` | `text/plain` | Rarely needed. | -| `"bytes"` | Custom (set via `contentType`) | Binary payloads (base64-encoded in `base64` field). | - -### Form body rules -- **Flat keys only.** Use Stripe's bracket notation for nested params: `"metadata[order_id]"`, `"items[0][price]"`. -- **No nested objects or arrays** in the value — the platform rejects them with a clear error. This avoids hidden flattening ambiguity. -- **Primitive values:** `string`, `number` (converted to decimal string), `boolean` (converted to `"true"` / `"false"`). -- **Empty string** is preserved (sends `key=`). -- **Null values** are omitted from the encoded body. - -## Stripe SDK via the HTTP proxy - -For Next.js apps you can also use the official `stripe` npm package and route every call through the Major HTTP proxy. The proxy injects the secret key, so you never touch credentials in app code. See [using-http-proxy](../http-proxy/SKILL.md) for the proxy reference. - -**Setup:** - -```bash -pnpm add stripe @major-tech/resource-client -``` - -**Usage (Server Components, Route Handlers, Server Actions):** - -```typescript -import Stripe from "stripe"; -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const proxyFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.STRIPE_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -const stripe = new Stripe("sk_unused_proxy_injects_real_key", { - httpClient: Stripe.createFetchHttpClient(proxyFetch), -}); - -// Use the SDK normally — every request flows through the proxy -const customers = await stripe.customers.list({ limit: 10 }); -const intent = await stripe.paymentIntents.create({ - amount: 2000, - currency: "usd", - customer: "cus_abc123", -}); -``` - -**Rules:** - -- `MAJOR_API_BASE_URL` and `MAJOR_JWT_TOKEN` are platform-managed env vars — assume they're set, don't ask the user to provide them. `STRIPE_RESOURCE_ID` is the UUID of the connected Stripe resource. -- The placeholder `"sk_unused_..."` constructor key is never sent — the proxy strips `Authorization` and injects the real secret key. -- Do NOT set the `Authorization` header anywhere. The proxy will strip it. -- This only works server-side (Server Components, Route Handlers, Server Actions). The `/next` subpath uses `next/headers`, which is not available in client components. -- Use a static `STRIPE_RESOURCE_ID` string literal. -- API version pinning still works — pass `apiVersion: "2026-03-25.preview"` in the Stripe constructor or `Stripe-Version` per-request via the SDK's standard mechanisms. - -## Tips - -- **Use `type: "form"` for Stripe v1 writes.** Stripe's v1 API natively expects form-encoded bodies. While `type: "json"` also works (Stripe accepts both), `type: "form"` is the canonical encoding documented by Stripe. -- **Paths must start with `/v1/` or `/v2/`.** The platform validates paths and rejects absolute URLs, protocol-relative paths, and paths outside `/v1/` or `/v2/`. -- **Pagination**: Stripe uses cursor-based pagination. Pass `starting_after` with the last object's ID to get the next page. Check `has_more` in the response. -- **All list endpoints** support `limit` (default 10, max 100). -- **Expand related objects**: Use the `expand[]` query param to inline related objects instead of just their IDs. -- **Test vs live keys**: Test mode keys start with `sk_test_`, live mode with `sk_live_`. The connector works with either — just provide the right key. -- **Stripe API versioning**: No `Stripe-Version` header is sent by default, so Stripe uses your account's default API version. Pass `headers: { "Stripe-Version": "..." }` to pin a specific version. -- **Common v1 endpoints**: `/v1/customers`, `/v1/payment_intents`, `/v1/subscriptions`, `/v1/invoices`, `/v1/charges`, `/v1/balance`, `/v1/refunds`, `/v1/products`, `/v1/prices` - -**Docs**: [Stripe API Reference](https://docs.stripe.com/api) diff --git a/plugins/shared/skills/resources_tiktokads/SKILL.md b/plugins/shared/skills/resources_tiktokads/SKILL.md deleted file mode 100644 index 48940e7..0000000 --- a/plugins/shared/skills/resources_tiktokads/SKILL.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: using-tiktokads-connector -description: Implements TikTok Marketing API access for ad accounts, campaigns, and ad reports using generated clients and MCP tools. Use when doing ANYTHING that touches the TikTok Marketing / Ads Manager API — advertisers, campaigns, ad groups, ads, or reporting — in any way, load this skill. ---- - -# Major Platform Resource: TikTok Marketing API - -Reference: https://business-api.tiktok.com/portal/docs - -## Common: Interacting with Resources - -**Security**: Never connect directly to TikTok APIs with raw credentials. Never put OAuth tokens in code, logs, env vars, prompts, or user-visible output. Always use generated clients or MCP tools. - -**Three ways to interact with TikTok Ads:** - -1. **MCP tools** (direct, no code needed): Tools follow the pattern `mcp__resources__tiktokads_`. Use `mcp__resources__list_resources` to discover available resources and their IDs. -2. **Generated TypeScript clients** (for app code): Call `mcp__resource-tools__add-resource-client` with a `resourceId` to generate a typed client. Clients are created in `/clients/` (Next.js) or `/src/clients/` (Vite). -3. **HTTP proxy** (Next.js apps): Use `createProxyFetch` from `@major-tech/resource-client/next` to call the TikTok Marketing API directly with automatic auth injection. See [using-http-proxy](../http-proxy/SKILL.md) for setup and usage — preferred when you need to hit endpoints not covered by MCP tools or the typed client, or when using an official SDK that accepts a custom `fetch`. - -**CRITICAL: Do NOT guess client method names or signatures.** The TypeScript clients in `@major-tech/resource-client` have strongly typed inputs and outputs. ALWAYS read the actual client source code in the generated `/clients/` directory (or the package itself) to verify available methods and exact signatures before writing app code. - -**Framework note**: Next.js = resource clients must be used in server-side code only (Server Components, Server Actions, API Routes). Vite = call directly from frontend. - -**Error handling**: Always check `result.ok` before accessing `result.result`. - -**Invocation keys must be static strings** - use descriptive literals like `"fetch-tiktok-campaigns"`, never dynamic values like `` `${advertiserId}-campaigns` ``. - ---- - -## Connector Model - -- The connector is OAuth-token scoped, not bound to one advertiser. -- TikTok's Marketing API advertiser flow returns a long-lived access token with **no refresh token** — when the token is revoked the admin sees a Reconnect prompt in the connector panel. Do not ask users for tokens. -- Most TikTok endpoints take an `advertiser_id` query param. Call `tiktokads_list_advertisers` first to discover authorized IDs. -- The connector's `accessMode` setting (`readonly` or `readwrite`) controls write safety — write methods are blocked when set to `readonly`. - -### What the connector injects automatically - -You never pass these — the connector adds them at call time: - -- **Access token**: every request gets the `Access-Token` header. -- **`app_id` and `secret`**: only for `/oauth2/advertiser/get/` (the list-advertisers endpoint), which authenticates the *app* in addition to the user. Both `tiktokads_list_advertisers` and `tikTokAdsClient.listAdvertisers()` get them automatically. - -What you DO need to pass yourself for advertiser-scoped endpoints (campaigns, ads, reports, etc.): `advertiser_id` in the query or body. Typed tools/methods take `advertiserId` as a positional arg; raw `invoke` callers must include it in `query` or `body` themselves. - -### Empty advertiser list - -If `tiktokads_list_advertisers` returns `data.list: []` with `code: 0, message: "OK"`, the OAuth grant succeeded but the user did not authorize any ad accounts on TikTok's consent screen. Almost every Marketing API endpoint needs an `advertiser_id`, so most tools will fail until they reconnect and pick at least one advertiser. Endpoints that don't need an advertiser (e.g. `/user/info/`) will still work. Tell the user to reconnect via the connector settings rather than retrying API calls. - ---- - -## MCP Tools - -- `mcp__resources__tiktokads_list_advertisers` - List advertiser/ad accounts authorized for this OAuth token. Args: `resourceId` -- `mcp__resources__tiktokads_list_campaigns` - List campaigns for one advertiser. Args: `resourceId`, `advertiserId`, `campaignIds?`, `campaignName?`, `page?`, `pageSize?` -- `mcp__resources__tiktokads_get_campaign` - Fetch one campaign by ID. Args: `resourceId`, `advertiserId`, `campaignId` -- `mcp__resources__tiktokads_run_report` - Run a Marketing API report. Args: `resourceId`, `advertiserId`, `metrics`, `reportType?`, `dataLevel?`, `dimensions?`, `startDate?`, `endDate?`, `page?`, `pageSize?` -- `mcp__resources__tiktokads_invoke` - Escape hatch for any TikTok Marketing API request under `/open_api/v1.3/`. Args: `resourceId`, `method`, `path`, `query?`, `body?`, `timeoutMs?` - -Prefer the typed tools over `tiktokads_invoke` when they cover the use case. Write methods through `tiktokads_invoke` require the connector's `accessMode` to be `readwrite`. - ---- - -## TypeScript Client - -Use the typed helpers for common Marketing API workflows. Use `invoke()` only when a helper does not cover the endpoint. - -```typescript -import { tiktokAdsClient } from "./clients"; - -const advertisers = await tiktokAdsClient.listAdvertisers("fetch-tiktok-advertisers"); - -if (!advertisers.ok) { - throw new Error(advertisers.error.message); -} - -const campaigns = await tiktokAdsClient.listCampaigns("1234567890", "fetch-tiktok-campaigns", { - pageSize: 20, -}); - -if (!campaigns.ok) { - throw new Error(campaigns.error.message); -} -``` - -For reports from app code, request only the metrics needed for the UI or report. - -```typescript -const report = await tiktokAdsClient.runReport( - "1234567890", - ["impressions", "clicks", "spend"], - "fetch-tiktok-report", - { - reportType: "BASIC", - dataLevel: "AUCTION_CAMPAIGN", - dimensions: ["campaign_id", "stat_time_day"], - startDate: "2026-04-01", - endDate: "2026-04-30", - }, -); - -if (!report.ok) { - throw new Error(report.error.message); -} -``` - -### Response shape — DO NOT GUESS - -Every typed method returns `TikTokAdsInvokeResponse`, which is -`BaseInvokeSuccess | InvokeFailure`: - -```typescript -{ - ok: true, - requestId: "...", - result: { - kind: "tiktokads", - operation: "listAdvertisers", // matches the called method - data: { // TikTok envelope, verbatim - code: 0, - message: "OK", - request_id: "...", - data: { list: [...], page_info: {...} } // the actual payload - } - } -} -``` - -**`res.result.data` IS the TikTok envelope** (`{ code, message, request_id, data }`). The actual payload (advertiser list, campaigns array, report rows, etc.) is one level deeper at `res.result.data.data`. There is no `body.value`, no `kind: "api"`, no triple-nested `.data`. - -`code: 0` means success. Non-zero codes carry a human-readable `message` that you should surface to the user. - -Concrete example for `listAdvertisers`: - -```typescript -const res = await tiktokAdsClient.listAdvertisers("fetch-tiktok-advertisers"); - -if (!res.ok) { - throw new Error(res.error.message); -} - -const envelope = res.result.data as { - code: number; - message: string; - data?: { list?: Array<{ advertiser_id: string; advertiser_name: string }> }; -}; - -if (envelope.code !== 0) { - throw new Error(`TikTok error: ${envelope.message}`); -} - -const advertiserList = envelope.data?.list ?? []; -``` - ---- - -## Raw Invoke Rules - -`tiktokads_invoke` is a thin API wrapper. Include TikTok-required fields exactly as TikTok expects them. The connector injects the access token automatically; for `/oauth2/advertiser/get/` it also injects `app_id` and `secret`. - -For advertiser-scoped endpoints, include `advertiser_id` in `query` or JSON `body` yourself: - -```json -{ - "method": "GET", - "path": "/campaign/get/", - "query": { - "advertiser_id": ["1234567890"] - } -} -``` - -Direct calls to `/oauth2/access_token` and `/oauth2/refresh_token` are blocked — authentication is handled by the connector. - ---- - -## TikTok API Notes - -- Most Marketing API list endpoints accept `filtering` as a JSON-stringified object inside the query (TikTok's convention). The typed `listCampaigns` builder handles this for you; raw `invoke` callers must stringify themselves. -- Reports require both `dimensions` and `metrics` to be JSON-stringified arrays in the query. -- TikTok responses use `{ code, message, data, request_id }` as the envelope. `code: 0` means success; non-zero codes carry a human-readable `message`. -- Access tokens for the advertiser flow are long-lived — there is no refresh. If a call fails with auth errors, ask the user to reconnect the resource. - -**Docs**: [TikTok Marketing API](https://business-api.tiktok.com/portal/docs) diff --git a/plugins/shared/skills/resources_zendesk/SKILL.md b/plugins/shared/skills/resources_zendesk/SKILL.md deleted file mode 100644 index a763dde..0000000 --- a/plugins/shared/skills/resources_zendesk/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: using-zendesk-connector -description: Implements Zendesk Support API access (tickets, search, users, comments) through the Major HTTP proxy. Use when doing ANYTHING that touches Zendesk in any way, load this skill. ---- - -# Major Platform Resource: Zendesk - -Zendesk is a proxy-only resource — every call goes through Major's HTTP proxy. There is **no typed `zendesk_*` MCP tool and no generated TypeScript client**. The proxy resolves the tenant host (`.zendesk.com`) at request time from the connected account, so you never hardcode or pass the subdomain — always pass a **leading-slash relative path** like `/api/v2/tickets.json`. - -**Security**: Never set the `Authorization` header — the proxy injects it. Reserved headers (`Authorization`, `Cookie`, `Host`, `Forwarded`, `X-Forwarded-*`, `X-Real-Ip`, `X-Major-*`) are stripped on the way out. - ---- - -## MCP Tools - -Use the generic HTTP proxy tools — there are no Zendesk-specific MCP tools. - -- `mcp__resources__http_proxy_get` — Read-only GET. Args: `resourceId`, `url` (leading-slash path), `headers?`, `timeoutMs?` -- `mcp__resources__http_proxy_invoke` — Any HTTP method. Args: `resourceId`, `method`, `url` (leading-slash path), `headers?`, `body?`, `timeoutMs?` - -See [using-http-proxy](../http-proxy/SKILL.md) for the full proxy reference. - -## HTTP Proxy via `createProxyFetch` (Next.js) - -For app code, drop the generic `createProxyFetch` into any SDK that takes a custom `fetch`, or use it as a plain `fetch` wrapper. Pass **leading-slash paths** instead of a full URL — the proxy resolves the upstream host. - -```typescript -import { createProxyFetch } from "@major-tech/resource-client/next"; - -const proxyFetch = createProxyFetch({ - baseUrl: process.env.MAJOR_API_BASE_URL!, - resourceId: process.env.ZENDESK_RESOURCE_ID!, - majorJwtToken: process.env.MAJOR_JWT_TOKEN!, -}); - -// List the 25 most recently updated tickets -const res = await proxyFetch("/api/v2/tickets.json?per_page=25&sort_by=updated_at&sort_order=desc"); -if (!res.ok) throw new Error(`Zendesk ${res.status}: ${await res.text()}`); -const { tickets } = await res.json(); - -// Search across tickets / users / orgs / articles -const search = await proxyFetch( - `/api/v2/search.json?query=${encodeURIComponent("type:ticket status.zendesk.com/...` will fail — the subdomain isn't known to app code. -- **`resourceId` must be a static string literal** — the query extractor only tracks proxy calls when the resource id is known at build time. -- Server-side only (Server Components, Route Handlers, Server Actions). The `/next` subpath uses `next/headers`. -- Do NOT set the `Authorization` header — the proxy injects upstream auth. - -## Common Zendesk endpoints - -| What | Method | Path | -| ------------------ | ------ | ------------------------------------------------------------- | -| List tickets | GET | `/api/v2/tickets.json` | -| Get one ticket | GET | `/api/v2/tickets/{id}.json` | -| Create a ticket | POST | `/api/v2/tickets.json` with body `{ "ticket": { ... } }` | -| Update a ticket | PUT | `/api/v2/tickets/{id}.json` with body `{ "ticket": { ... } }` | -| Search | GET | `/api/v2/search.json?query=type:ticket+status