chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# Tools Scope
These rules apply to integration tool definitions under `apps/sim/tools/**`.
- Start from the service API docs before adding or changing a tool.
- Keep each service under `tools/{service}/` with `index.ts`, `types.ts`, and one file per action.
- Tool IDs must use `snake_case` and match registry keys exactly.
- Use `visibility: 'hidden'` only for system-injected params such as OAuth access tokens.
- Use `visibility: 'user-only'` for credentials and account-specific values the user must provide.
- Use `visibility: 'user-or-llm'` for ordinary operation parameters.
- In `transformResponse`, extract meaningful fields instead of dumping raw JSON.
- Use `?? null` for nullable response fields and `?? []` for optional arrays where appropriate.
- Register every tool in `tools/registry.ts` and keep entries aligned with the exported tool IDs.
+52
View File
@@ -0,0 +1,52 @@
import type { A2ACancelTaskParams, A2ACancelTaskResponse } from '@/tools/a2a/types'
import type { ToolConfig } from '@/tools/types'
export const a2aCancelTaskTool: ToolConfig<A2ACancelTaskParams, A2ACancelTaskResponse> = {
id: 'a2a_cancel_task',
name: 'A2A Cancel Task',
description: 'Request cancellation of an in-progress A2A task.',
version: '1.0.0',
params: {
agentUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The A2A agent endpoint URL',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The task ID to cancel',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'API key for authentication (if required)',
},
},
request: {
url: '/api/tools/a2a/cancel-task',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const body: Record<string, unknown> = {
agentUrl: params.agentUrl,
taskId: params.taskId,
}
if (params.apiKey) body.apiKey = params.apiKey
return body
},
},
transformResponse: async (response: Response) => response.json(),
outputs: {
taskId: { type: 'string', description: 'Task identifier' },
state: { type: 'string', description: 'Task lifecycle state after cancellation' },
canceled: { type: 'boolean', description: 'Whether the task was canceled' },
},
}
+76
View File
@@ -0,0 +1,76 @@
import type { A2AAgentCardResponse, A2AGetAgentCardParams } from '@/tools/a2a/types'
import type { ToolConfig } from '@/tools/types'
export const a2aGetAgentCardTool: ToolConfig<A2AGetAgentCardParams, A2AAgentCardResponse> = {
id: 'a2a_get_agent_card',
name: 'A2A Get Agent Card',
description: 'Fetch the Agent Card (discovery document) for an external A2A agent.',
version: '1.0.0',
params: {
agentUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The A2A agent endpoint URL',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'API key for authentication (if required)',
},
},
request: {
url: '/api/tools/a2a/get-agent-card',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const body: Record<string, unknown> = { agentUrl: params.agentUrl }
if (params.apiKey) body.apiKey = params.apiKey
return body
},
},
transformResponse: async (response: Response) => response.json(),
outputs: {
name: { type: 'string', description: 'Agent display name' },
description: { type: 'string', description: 'Agent description' },
url: { type: 'string', description: 'Agent endpoint URL' },
version: { type: 'string', description: "The agent's own version" },
protocolVersion: { type: 'string', description: 'A2A protocol version the agent exposes' },
capabilities: {
type: 'json',
description: 'Agent capability flags',
properties: {
streaming: { type: 'boolean', description: 'Supports streaming responses' },
pushNotifications: { type: 'boolean', description: 'Supports push notifications' },
extendedAgentCard: { type: 'boolean', description: 'Provides an extended agent card' },
},
},
skills: {
type: 'array',
description: 'Skills the agent can perform',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Skill identifier' },
name: { type: 'string', description: 'Skill name' },
description: { type: 'string', description: 'Skill description' },
},
},
},
defaultInputModes: {
type: 'array',
description: 'Default accepted input media types',
items: { type: 'string' },
},
defaultOutputModes: {
type: 'array',
description: 'Default produced output media types',
items: { type: 'string' },
},
},
}
+55
View File
@@ -0,0 +1,55 @@
import { A2A_TASK_OUTPUTS, type A2AGetTaskParams, type A2ATaskResponse } from '@/tools/a2a/types'
import type { ToolConfig } from '@/tools/types'
export const a2aGetTaskTool: ToolConfig<A2AGetTaskParams, A2ATaskResponse> = {
id: 'a2a_get_task',
name: 'A2A Get Task',
description: 'Retrieve the current state and result of an A2A task.',
version: '1.0.0',
params: {
agentUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The A2A agent endpoint URL',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The task ID to retrieve',
},
historyLength: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of history messages to include',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'API key for authentication (if required)',
},
},
request: {
url: '/api/tools/a2a/get-task',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const body: Record<string, unknown> = {
agentUrl: params.agentUrl,
taskId: params.taskId,
}
if (params.historyLength !== undefined) body.historyLength = params.historyLength
if (params.apiKey) body.apiKey = params.apiKey
return body
},
},
transformResponse: async (response: Response) => response.json(),
outputs: A2A_TASK_OUTPUTS,
}
+5
View File
@@ -0,0 +1,5 @@
export { a2aCancelTaskTool } from './cancel_task'
export { a2aGetAgentCardTool } from './get_agent_card'
export { a2aGetTaskTool } from './get_task'
export { a2aSendMessageTool } from './send_message'
export * from './types'
+80
View File
@@ -0,0 +1,80 @@
import {
A2A_TASK_OUTPUTS,
type A2ASendMessageParams,
type A2ATaskResponse,
} from '@/tools/a2a/types'
import type { ToolConfig } from '@/tools/types'
export const a2aSendMessageTool: ToolConfig<A2ASendMessageParams, A2ATaskResponse> = {
id: 'a2a_send_message',
name: 'A2A Send Message',
description: 'Send a message to an external A2A agent and return its response.',
version: '1.0.0',
params: {
agentUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The A2A agent endpoint URL',
},
message: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The message text to send',
},
data: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Optional structured JSON data to attach',
},
files: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Optional files to attach',
},
taskId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Existing task ID to continue',
},
contextId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Conversation context ID to continue',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'API key for authentication (if required)',
},
},
request: {
url: '/api/tools/a2a/send-message',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const body: Record<string, unknown> = {
agentUrl: params.agentUrl,
message: params.message,
}
if (params.data) body.data = params.data
if (params.files) body.files = params.files
if (params.taskId) body.taskId = params.taskId
if (params.contextId) body.contextId = params.contextId
if (params.apiKey) body.apiKey = params.apiKey
return body
},
},
transformResponse: async (response: Response) => response.json(),
outputs: A2A_TASK_OUTPUTS,
}
+63
View File
@@ -0,0 +1,63 @@
import type { A2AAgentCardOutput, A2ATaskOutput } from '@/lib/a2a/client'
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
export interface A2ABaseParams {
agentUrl: string
apiKey?: string
}
export interface A2ASendMessageParams extends A2ABaseParams {
message: string
data?: unknown
files?: UserFile[]
taskId?: string
contextId?: string
}
export interface A2AGetTaskParams extends A2ABaseParams {
taskId: string
historyLength?: number
}
export interface A2ACancelTaskParams extends A2ABaseParams {
taskId: string
}
export type A2AGetAgentCardParams = A2ABaseParams
export interface A2ATaskResponse extends ToolResponse {
output: A2ATaskOutput
}
export interface A2ACancelTaskResponse extends ToolResponse {
output: {
taskId: string
state: string
canceled: boolean
}
}
export interface A2AAgentCardResponse extends ToolResponse {
output: A2AAgentCardOutput
}
/** Shared output schema for the task-returning operations (send, get). */
export const A2A_TASK_OUTPUTS = {
content: { type: 'string', description: 'Agent response text' },
taskId: { type: 'string', description: 'Task identifier' },
contextId: { type: 'string', description: 'Conversation/context identifier' },
state: { type: 'string', description: 'Task lifecycle state' },
artifacts: {
type: 'array',
description: 'Structured task output artifacts',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Artifact name' },
description: { type: 'string', description: 'Artifact description' },
content: { type: 'string', description: 'Artifact text content' },
},
},
},
} as const satisfies Record<string, OutputProperty>
+164
View File
@@ -0,0 +1,164 @@
import type { CreateDraftParams, CreateDraftResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailCreateDraftTool: ToolConfig<CreateDraftParams, CreateDraftResult> = {
id: 'agentmail_create_draft',
name: 'Create Draft',
description: 'Create a new email draft in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to create the draft in',
},
to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Recipient email addresses (comma-separated)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Draft subject line',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Plain text draft body',
},
html: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTML draft body',
},
cc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CC recipient email addresses (comma-separated)',
},
bcc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'BCC recipient email addresses (comma-separated)',
},
inReplyTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID of message being replied to',
},
sendAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO 8601 timestamp to schedule sending',
},
},
request: {
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
if (params.subject) body.subject = params.subject
if (params.text) body.text = params.text
if (params.html) body.html = params.html
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
if (params.inReplyTo) body.in_reply_to = params.inReplyTo
if (params.sendAt) body.send_at = params.sendAt
return body
},
},
transformResponse: async (response): Promise<CreateDraftResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to create draft',
output: {
draftId: '',
inboxId: '',
subject: null,
to: [],
cc: [],
bcc: [],
text: null,
html: null,
preview: null,
labels: [],
inReplyTo: null,
sendStatus: null,
sendAt: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
draftId: data.draft_id ?? '',
inboxId: data.inbox_id ?? '',
subject: data.subject ?? null,
to: data.to ?? [],
cc: data.cc ?? [],
bcc: data.bcc ?? [],
text: data.text ?? null,
html: data.html ?? null,
preview: data.preview ?? null,
labels: data.labels ?? [],
inReplyTo: data.in_reply_to ?? null,
sendStatus: data.send_status ?? null,
sendAt: data.send_at ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
draftId: { type: 'string', description: 'Unique identifier for the draft' },
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
subject: { type: 'string', description: 'Draft subject', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
text: { type: 'string', description: 'Plain text content', optional: true },
html: { type: 'string', description: 'HTML content', optional: true },
preview: { type: 'string', description: 'Draft preview text', optional: true },
labels: { type: 'array', description: 'Labels assigned to the draft' },
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
sendStatus: {
type: 'string',
description: 'Send status (scheduled, sending, failed)',
optional: true,
},
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
+87
View File
@@ -0,0 +1,87 @@
import type { CreateInboxParams, CreateInboxResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailCreateInboxTool: ToolConfig<CreateInboxParams, CreateInboxResult> = {
id: 'agentmail_create_inbox',
name: 'Create Inbox',
description: 'Create a new email inbox with AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
username: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Username for the inbox email address',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Domain for the inbox email address',
},
displayName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Display name for the inbox',
},
},
request: {
url: 'https://api.agentmail.to/v0/inboxes',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
...(params.username && { username: params.username }),
...(params.domain && { domain: params.domain }),
...(params.displayName && { display_name: params.displayName }),
}),
},
transformResponse: async (response): Promise<CreateInboxResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to create inbox',
output: {
inboxId: '',
email: '',
displayName: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
inboxId: data.inbox_id ?? '',
email: data.email ?? '',
displayName: data.display_name ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
email: { type: 'string', description: 'Email address of the inbox' },
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
+59
View File
@@ -0,0 +1,59 @@
import type { DeleteDraftParams, DeleteDraftResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailDeleteDraftTool: ToolConfig<DeleteDraftParams, DeleteDraftResult> = {
id: 'agentmail_delete_draft',
name: 'Delete Draft',
description: 'Delete an email draft in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the draft',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to delete',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<DeleteDraftResult> => {
if (!response.ok) {
const data = await response.json()
return {
success: false,
error: data.message ?? 'Failed to delete draft',
output: { deleted: false },
}
}
return {
success: true,
output: { deleted: true },
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the draft was successfully deleted' },
},
}
+52
View File
@@ -0,0 +1,52 @@
import type { DeleteInboxParams, DeleteInboxResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailDeleteInboxTool: ToolConfig<DeleteInboxParams, DeleteInboxResult> = {
id: 'agentmail_delete_inbox',
name: 'Delete Inbox',
description: 'Delete an email inbox in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to delete',
},
},
request: {
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<DeleteInboxResult> => {
if (!response.ok) {
const data = await response.json()
return {
success: false,
error: data.message ?? 'Failed to delete inbox',
output: { deleted: false },
}
}
return {
success: true,
output: { deleted: true },
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the inbox was successfully deleted' },
},
}
+70
View File
@@ -0,0 +1,70 @@
import type { DeleteThreadParams, DeleteThreadResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailDeleteThreadTool: ToolConfig<DeleteThreadParams, DeleteThreadResult> = {
id: 'agentmail_delete_thread',
name: 'Delete Thread',
description:
'Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the thread',
},
threadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the thread to delete',
},
permanent: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Force permanent deletion instead of moving to trash',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.permanent) query.set('permanent', 'true')
const qs = query.toString()
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<DeleteThreadResult> => {
if (!response.ok) {
const data = await response.json()
return {
success: false,
error: data.message ?? 'Failed to delete thread',
output: { deleted: false },
}
}
return {
success: true,
output: { deleted: true },
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the thread was successfully deleted' },
},
}
+112
View File
@@ -0,0 +1,112 @@
import type { ForwardMessageParams, ForwardMessageResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailForwardMessageTool: ToolConfig<ForwardMessageParams, ForwardMessageResult> = {
id: 'agentmail_forward_message',
name: 'Forward Message',
description: 'Forward an email message to new recipients in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the message',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the message to forward',
},
to: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Recipient email addresses (comma-separated)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Override subject line',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Additional plain text to prepend',
},
html: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Additional HTML to prepend',
},
cc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CC recipient email addresses (comma-separated)',
},
bcc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'BCC recipient email addresses (comma-separated)',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}/forward`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
to: params.to.split(',').map((e) => e.trim()),
}
if (params.subject) body.subject = params.subject
if (params.text) body.text = params.text
if (params.html) body.html = params.html
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
return body
},
},
transformResponse: async (response): Promise<ForwardMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to forward message',
output: { messageId: '', threadId: '' },
}
}
return {
success: true,
output: {
messageId: data.message_id ?? '',
threadId: data.thread_id ?? '',
},
}
},
outputs: {
messageId: { type: 'string', description: 'ID of the forwarded message' },
threadId: { type: 'string', description: 'ID of the thread' },
},
}
+110
View File
@@ -0,0 +1,110 @@
import type { GetDraftParams, GetDraftResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailGetDraftTool: ToolConfig<GetDraftParams, GetDraftResult> = {
id: 'agentmail_get_draft',
name: 'Get Draft',
description: 'Get details of a specific email draft in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox the draft belongs to',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to retrieve',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<GetDraftResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to get draft',
output: {
draftId: '',
inboxId: '',
subject: null,
to: [],
cc: [],
bcc: [],
text: null,
html: null,
preview: null,
labels: [],
inReplyTo: null,
sendStatus: null,
sendAt: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
draftId: data.draft_id ?? '',
inboxId: data.inbox_id ?? '',
subject: data.subject ?? null,
to: data.to ?? [],
cc: data.cc ?? [],
bcc: data.bcc ?? [],
text: data.text ?? null,
html: data.html ?? null,
preview: data.preview ?? null,
labels: data.labels ?? [],
inReplyTo: data.in_reply_to ?? null,
sendStatus: data.send_status ?? null,
sendAt: data.send_at ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
draftId: { type: 'string', description: 'Unique identifier for the draft' },
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
subject: { type: 'string', description: 'Draft subject', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
text: { type: 'string', description: 'Plain text content', optional: true },
html: { type: 'string', description: 'HTML content', optional: true },
preview: { type: 'string', description: 'Draft preview text', optional: true },
labels: { type: 'array', description: 'Labels assigned to the draft' },
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
sendStatus: {
type: 'string',
description: 'Send status (scheduled, sending, failed)',
optional: true,
},
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
+69
View File
@@ -0,0 +1,69 @@
import type { GetInboxParams, GetInboxResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailGetInboxTool: ToolConfig<GetInboxParams, GetInboxResult> = {
id: 'agentmail_get_inbox',
name: 'Get Inbox',
description: 'Get details of a specific email inbox in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to retrieve',
},
},
request: {
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<GetInboxResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to get inbox',
output: {
inboxId: '',
email: '',
displayName: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
inboxId: data.inbox_id ?? '',
email: data.email ?? '',
displayName: data.display_name ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
email: { type: 'string', description: 'Email address of the inbox' },
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
+101
View File
@@ -0,0 +1,101 @@
import type { GetMessageParams, GetMessageResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailGetMessageTool: ToolConfig<GetMessageParams, GetMessageResult> = {
id: 'agentmail_get_message',
name: 'Get Message',
description: 'Get details of a specific email message in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the message',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the message to retrieve',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<GetMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to get message',
output: {
messageId: '',
threadId: '',
from: null,
to: [],
cc: [],
bcc: [],
subject: null,
text: null,
html: null,
labels: [],
timestamp: null,
createdAt: '',
},
}
}
return {
success: true,
output: {
messageId: data.message_id ?? '',
threadId: data.thread_id ?? '',
from: data.from ?? null,
to: data.to ?? [],
cc: data.cc ?? [],
bcc: data.bcc ?? [],
subject: data.subject ?? null,
text: data.text ?? null,
html: data.html ?? null,
labels: data.labels ?? [],
timestamp: data.timestamp ?? null,
createdAt: data.created_at ?? '',
},
}
},
outputs: {
messageId: { type: 'string', description: 'Unique identifier for the message' },
threadId: { type: 'string', description: 'ID of the thread this message belongs to' },
from: { type: 'string', description: 'Sender email address', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
subject: { type: 'string', description: 'Message subject', optional: true },
text: { type: 'string', description: 'Plain text content', optional: true },
html: { type: 'string', description: 'HTML content', optional: true },
labels: { type: 'array', description: 'Labels assigned to the message' },
timestamp: {
type: 'string',
description: 'Time the message was sent or drafted',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp' },
},
}
+126
View File
@@ -0,0 +1,126 @@
import type { GetThreadParams, GetThreadResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailGetThreadTool: ToolConfig<GetThreadParams, GetThreadResult> = {
id: 'agentmail_get_thread',
name: 'Get Thread',
description: 'Get details of a specific email thread including messages in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the thread',
},
threadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the thread to retrieve',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<GetThreadResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to get thread',
output: {
threadId: '',
subject: null,
senders: [],
recipients: [],
messageCount: 0,
labels: [],
lastMessageAt: null,
createdAt: '',
updatedAt: '',
messages: [],
},
}
}
return {
success: true,
output: {
threadId: data.thread_id ?? '',
subject: data.subject ?? null,
senders: data.senders ?? [],
recipients: data.recipients ?? [],
messageCount: data.message_count ?? 0,
labels: data.labels ?? [],
lastMessageAt: data.timestamp ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
messages: (data.messages ?? []).map((msg: Record<string, unknown>) => ({
messageId: msg.message_id ?? '',
from: (msg.from as string) ?? null,
to: (msg.to as string[]) ?? [],
cc: (msg.cc as string[]) ?? [],
bcc: (msg.bcc as string[]) ?? [],
subject: (msg.subject as string) ?? null,
text: (msg.text as string) ?? null,
html: (msg.html as string) ?? null,
labels: (msg.labels as string[]) ?? [],
timestamp: (msg.timestamp as string) ?? null,
createdAt: (msg.created_at as string) ?? '',
})),
},
}
},
outputs: {
threadId: { type: 'string', description: 'Unique identifier for the thread' },
subject: { type: 'string', description: 'Thread subject', optional: true },
senders: { type: 'array', description: 'List of sender email addresses' },
recipients: { type: 'array', description: 'List of recipient email addresses' },
messageCount: { type: 'number', description: 'Number of messages in the thread' },
labels: { type: 'array', description: 'Labels assigned to the thread' },
lastMessageAt: { type: 'string', description: 'Timestamp of last message', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
messages: {
type: 'array',
description: 'Messages in the thread',
items: {
type: 'object',
properties: {
messageId: { type: 'string', description: 'Unique identifier for the message' },
from: { type: 'string', description: 'Sender email address', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
subject: { type: 'string', description: 'Message subject', optional: true },
text: { type: 'string', description: 'Plain text content', optional: true },
html: { type: 'string', description: 'HTML content', optional: true },
labels: { type: 'array', description: 'Labels assigned to the message' },
timestamp: {
type: 'string',
description: 'Time the message was sent or drafted',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp' },
},
},
},
},
}
+21
View File
@@ -0,0 +1,21 @@
export { agentmailCreateDraftTool } from '@/tools/agentmail/create_draft'
export { agentmailCreateInboxTool } from '@/tools/agentmail/create_inbox'
export { agentmailDeleteDraftTool } from '@/tools/agentmail/delete_draft'
export { agentmailDeleteInboxTool } from '@/tools/agentmail/delete_inbox'
export { agentmailDeleteThreadTool } from '@/tools/agentmail/delete_thread'
export { agentmailForwardMessageTool } from '@/tools/agentmail/forward_message'
export { agentmailGetDraftTool } from '@/tools/agentmail/get_draft'
export { agentmailGetInboxTool } from '@/tools/agentmail/get_inbox'
export { agentmailGetMessageTool } from '@/tools/agentmail/get_message'
export { agentmailGetThreadTool } from '@/tools/agentmail/get_thread'
export { agentmailListDraftsTool } from '@/tools/agentmail/list_drafts'
export { agentmailListInboxesTool } from '@/tools/agentmail/list_inboxes'
export { agentmailListMessagesTool } from '@/tools/agentmail/list_messages'
export { agentmailListThreadsTool } from '@/tools/agentmail/list_threads'
export { agentmailReplyMessageTool } from '@/tools/agentmail/reply_message'
export { agentmailSendDraftTool } from '@/tools/agentmail/send_draft'
export { agentmailSendMessageTool } from '@/tools/agentmail/send_message'
export { agentmailUpdateDraftTool } from '@/tools/agentmail/update_draft'
export { agentmailUpdateInboxTool } from '@/tools/agentmail/update_inbox'
export { agentmailUpdateMessageTool } from '@/tools/agentmail/update_message'
export { agentmailUpdateThreadTool } from '@/tools/agentmail/update_thread'
+116
View File
@@ -0,0 +1,116 @@
import type { ListDraftsParams, ListDraftsResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailListDraftsTool: ToolConfig<ListDraftsParams, ListDraftsResult> = {
id: 'agentmail_list_drafts',
name: 'List Drafts',
description: 'List email drafts in an inbox in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to list drafts from',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of drafts to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.pageToken) query.set('page_token', params.pageToken)
const qs = query.toString()
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<ListDraftsResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to list drafts',
output: { drafts: [], count: 0, nextPageToken: null },
}
}
return {
success: true,
output: {
drafts: (data.drafts ?? []).map((draft: Record<string, unknown>) => ({
draftId: draft.draft_id ?? '',
inboxId: draft.inbox_id ?? '',
subject: (draft.subject as string) ?? null,
to: (draft.to as string[]) ?? [],
cc: (draft.cc as string[]) ?? [],
bcc: (draft.bcc as string[]) ?? [],
preview: (draft.preview as string) ?? null,
sendStatus: (draft.send_status as string) ?? null,
sendAt: (draft.send_at as string) ?? null,
createdAt: (draft.created_at as string) ?? '',
updatedAt: (draft.updated_at as string) ?? '',
})),
count: data.count ?? 0,
nextPageToken: data.next_page_token ?? null,
},
}
},
outputs: {
drafts: {
type: 'array',
description: 'List of drafts',
items: {
type: 'object',
properties: {
draftId: { type: 'string', description: 'Unique identifier for the draft' },
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
subject: { type: 'string', description: 'Draft subject', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
preview: { type: 'string', description: 'Draft preview text', optional: true },
sendStatus: {
type: 'string',
description: 'Send status (scheduled, sending, failed)',
optional: true,
},
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
},
},
count: { type: 'number', description: 'Total number of drafts' },
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page',
optional: true,
},
},
}
+98
View File
@@ -0,0 +1,98 @@
import type { ListInboxesParams, ListInboxesResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailListInboxesTool: ToolConfig<ListInboxesParams, ListInboxesResult> = {
id: 'agentmail_list_inboxes',
name: 'List Inboxes',
description: 'List all email inboxes in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of inboxes to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.pageToken) query.set('page_token', params.pageToken)
const qs = query.toString()
return `https://api.agentmail.to/v0/inboxes${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<ListInboxesResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to list inboxes',
output: { inboxes: [], count: 0, nextPageToken: null },
}
}
return {
success: true,
output: {
inboxes: (data.inboxes ?? []).map((inbox: Record<string, unknown>) => ({
inboxId: inbox.inbox_id ?? '',
email: inbox.email ?? '',
displayName: inbox.display_name ?? null,
createdAt: inbox.created_at ?? '',
updatedAt: inbox.updated_at ?? '',
})),
count: data.count ?? 0,
nextPageToken: data.next_page_token ?? null,
},
}
},
outputs: {
inboxes: {
type: 'array',
description: 'List of inboxes',
items: {
type: 'object',
properties: {
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
email: { type: 'string', description: 'Email address of the inbox' },
displayName: {
type: 'string',
description: 'Display name of the inbox',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
},
},
count: { type: 'number', description: 'Total number of inboxes' },
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page',
optional: true,
},
},
}
+108
View File
@@ -0,0 +1,108 @@
import type { ListMessagesParams, ListMessagesResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailListMessagesTool: ToolConfig<ListMessagesParams, ListMessagesResult> = {
id: 'agentmail_list_messages',
name: 'List Messages',
description: 'List messages in an inbox in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to list messages from',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of messages to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.pageToken) query.set('page_token', params.pageToken)
const qs = query.toString()
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<ListMessagesResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to list messages',
output: { messages: [], count: 0, nextPageToken: null },
}
}
return {
success: true,
output: {
messages: (data.messages ?? []).map((msg: Record<string, unknown>) => ({
messageId: msg.message_id ?? '',
from: (msg.from as string) ?? null,
to: (msg.to as string[]) ?? [],
subject: (msg.subject as string) ?? null,
preview: (msg.preview as string) ?? null,
timestamp: (msg.timestamp as string) ?? null,
createdAt: (msg.created_at as string) ?? '',
})),
count: data.count ?? 0,
nextPageToken: data.next_page_token ?? null,
},
}
},
outputs: {
messages: {
type: 'array',
description: 'List of messages in the inbox',
items: {
type: 'object',
properties: {
messageId: { type: 'string', description: 'Unique identifier for the message' },
from: { type: 'string', description: 'Sender email address', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
subject: { type: 'string', description: 'Message subject', optional: true },
preview: { type: 'string', description: 'Message preview text', optional: true },
timestamp: {
type: 'string',
description: 'Time the message was sent or drafted',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp' },
},
},
},
count: { type: 'number', description: 'Total number of messages' },
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page',
optional: true,
},
},
}
+135
View File
@@ -0,0 +1,135 @@
import type { ListThreadsParams, ListThreadsResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailListThreadsTool: ToolConfig<ListThreadsParams, ListThreadsResult> = {
id: 'agentmail_list_threads',
name: 'List Threads',
description: 'List email threads in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to list threads from',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of threads to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
labels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated labels to filter threads by',
},
before: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter threads before this ISO 8601 timestamp',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter threads after this ISO 8601 timestamp',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.pageToken) query.set('page_token', params.pageToken)
if (params.labels) {
for (const label of params.labels.split(',')) {
query.append('labels', label.trim())
}
}
if (params.before) query.set('before', params.before)
if (params.after) query.set('after', params.after)
const qs = query.toString()
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<ListThreadsResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to list threads',
output: { threads: [], count: 0, nextPageToken: null },
}
}
return {
success: true,
output: {
threads: (data.threads ?? []).map((thread: Record<string, unknown>) => ({
threadId: thread.thread_id ?? '',
subject: (thread.subject as string) ?? null,
senders: (thread.senders as string[]) ?? [],
recipients: (thread.recipients as string[]) ?? [],
messageCount: (thread.message_count as number) ?? 0,
lastMessageAt: (thread.timestamp as string) ?? null,
createdAt: (thread.created_at as string) ?? '',
updatedAt: (thread.updated_at as string) ?? '',
})),
count: data.count ?? 0,
nextPageToken: data.next_page_token ?? null,
},
}
},
outputs: {
threads: {
type: 'array',
description: 'List of email threads',
items: {
type: 'object',
properties: {
threadId: { type: 'string', description: 'Unique identifier for the thread' },
subject: { type: 'string', description: 'Thread subject', optional: true },
senders: { type: 'array', description: 'List of sender email addresses' },
recipients: { type: 'array', description: 'List of recipient email addresses' },
messageCount: { type: 'number', description: 'Number of messages in the thread' },
lastMessageAt: {
type: 'string',
description: 'Timestamp of last message',
optional: true,
},
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
},
},
count: { type: 'number', description: 'Total number of threads' },
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page',
optional: true,
},
},
}
+115
View File
@@ -0,0 +1,115 @@
import type { ReplyMessageParams, ReplyMessageResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailReplyMessageTool: ToolConfig<ReplyMessageParams, ReplyMessageResult> = {
id: 'agentmail_reply_message',
name: 'Reply to Message',
description: 'Reply to an existing email message in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to reply from',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the message to reply to',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Plain text reply body',
},
html: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTML reply body',
},
to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Override recipient email addresses (comma-separated)',
},
cc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CC email addresses (comma-separated)',
},
bcc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'BCC email addresses (comma-separated)',
},
replyAll: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Reply to all recipients of the original message',
},
},
request: {
url: (params) => {
const endpoint = params.replyAll ? 'reply-all' : 'reply'
return `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}/${endpoint}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.text) body.text = params.text
if (params.html) body.html = params.html
// /reply-all endpoint auto-determines recipients; only /reply accepts to/cc/bcc
if (!params.replyAll) {
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
}
return body
},
},
transformResponse: async (response): Promise<ReplyMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to reply to message',
output: { messageId: '', threadId: '' },
}
}
return {
success: true,
output: {
messageId: data.message_id ?? '',
threadId: data.thread_id ?? '',
},
}
},
outputs: {
messageId: { type: 'string', description: 'ID of the sent reply message' },
threadId: { type: 'string', description: 'ID of the thread' },
},
}
+66
View File
@@ -0,0 +1,66 @@
import type { SendDraftParams, SendDraftResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailSendDraftTool: ToolConfig<SendDraftParams, SendDraftResult> = {
id: 'agentmail_send_draft',
name: 'Send Draft',
description: 'Send an existing email draft in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the draft',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to send',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}/send`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: () => ({}),
},
transformResponse: async (response): Promise<SendDraftResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to send draft',
output: { messageId: '', threadId: '' },
}
}
return {
success: true,
output: {
messageId: data.message_id ?? '',
threadId: data.thread_id ?? '',
},
}
},
outputs: {
messageId: { type: 'string', description: 'ID of the sent message' },
threadId: { type: 'string', description: 'ID of the thread' },
},
}
+114
View File
@@ -0,0 +1,114 @@
import type { SendMessageParams, SendMessageResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailSendMessageTool: ToolConfig<SendMessageParams, SendMessageResult> = {
id: 'agentmail_send_message',
name: 'Send Message',
description: 'Send an email message from an AgentMail inbox',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to send from',
},
to: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Recipient email address (comma-separated for multiple)',
},
subject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Email subject line',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Plain text email body',
},
html: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTML email body',
},
cc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CC recipient email addresses (comma-separated)',
},
bcc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'BCC recipient email addresses (comma-separated)',
},
},
request: {
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/send`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
to: params.to.split(',').map((e) => e.trim()),
subject: params.subject,
}
if (params.text) body.text = params.text
if (params.html) body.html = params.html
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
return body
},
},
transformResponse: async (response, params): Promise<SendMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to send message',
output: {
threadId: '',
messageId: '',
subject: '',
to: '',
},
}
}
return {
success: true,
output: {
threadId: data.thread_id ?? '',
messageId: data.message_id ?? '',
subject: params?.subject ?? '',
to: params?.to ?? '',
},
}
},
outputs: {
threadId: { type: 'string', description: 'ID of the created thread' },
messageId: { type: 'string', description: 'ID of the sent message' },
subject: { type: 'string', description: 'Email subject line' },
to: { type: 'string', description: 'Recipient email address' },
},
}
+452
View File
@@ -0,0 +1,452 @@
import type { ToolResponse } from '@/tools/types'
/** Create Inbox */
export interface CreateInboxParams {
apiKey: string
username?: string
domain?: string
displayName?: string
}
export interface CreateInboxResult extends ToolResponse {
output: {
inboxId: string
email: string
displayName: string | null
createdAt: string
updatedAt: string
}
}
/** List Inboxes */
export interface ListInboxesParams {
apiKey: string
limit?: number
pageToken?: string
}
export interface ListInboxesResult extends ToolResponse {
output: {
inboxes: Array<{
inboxId: string
email: string
displayName: string | null
createdAt: string
updatedAt: string
}>
count: number
nextPageToken: string | null
}
}
/** Get Inbox */
export interface GetInboxParams {
apiKey: string
inboxId: string
}
export interface GetInboxResult extends ToolResponse {
output: {
inboxId: string
email: string
displayName: string | null
createdAt: string
updatedAt: string
}
}
/** Update Inbox */
export interface UpdateInboxParams {
apiKey: string
inboxId: string
displayName: string
}
export interface UpdateInboxResult extends ToolResponse {
output: {
inboxId: string
email: string
displayName: string | null
createdAt: string
updatedAt: string
}
}
/** Delete Inbox */
export interface DeleteInboxParams {
apiKey: string
inboxId: string
}
export interface DeleteInboxResult extends ToolResponse {
output: {
deleted: boolean
}
}
/** Send Message (Create Thread) */
export interface SendMessageParams {
apiKey: string
inboxId: string
to: string
subject: string
text?: string
html?: string
cc?: string
bcc?: string
}
export interface SendMessageResult extends ToolResponse {
output: {
threadId: string
messageId: string
subject: string
to: string
}
}
/** Reply to Message */
export interface ReplyMessageParams {
apiKey: string
inboxId: string
messageId: string
text?: string
html?: string
to?: string
cc?: string
bcc?: string
replyAll?: boolean
}
export interface ReplyMessageResult extends ToolResponse {
output: {
messageId: string
threadId: string
}
}
/** Forward Message */
export interface ForwardMessageParams {
apiKey: string
inboxId: string
messageId: string
to: string
subject?: string
text?: string
html?: string
cc?: string
bcc?: string
}
export interface ForwardMessageResult extends ToolResponse {
output: {
messageId: string
threadId: string
}
}
/** Update Message Labels */
export interface UpdateMessageParams {
apiKey: string
inboxId: string
messageId: string
addLabels?: string
removeLabels?: string
}
export interface UpdateMessageResult extends ToolResponse {
output: {
messageId: string
labels: string[]
}
}
/** Create Draft */
export interface CreateDraftParams {
apiKey: string
inboxId: string
to?: string
subject?: string
text?: string
html?: string
cc?: string
bcc?: string
inReplyTo?: string
sendAt?: string
}
export interface CreateDraftResult extends ToolResponse {
output: {
draftId: string
inboxId: string
subject: string | null
to: string[]
cc: string[]
bcc: string[]
text: string | null
html: string | null
preview: string | null
labels: string[]
inReplyTo: string | null
sendStatus: string | null
sendAt: string | null
createdAt: string
updatedAt: string
}
}
/** Update Draft */
export interface UpdateDraftParams {
apiKey: string
inboxId: string
draftId: string
to?: string
subject?: string
text?: string
html?: string
cc?: string
bcc?: string
sendAt?: string
}
export interface UpdateDraftResult extends ToolResponse {
output: {
draftId: string
inboxId: string
subject: string | null
to: string[]
cc: string[]
bcc: string[]
text: string | null
html: string | null
preview: string | null
labels: string[]
inReplyTo: string | null
sendStatus: string | null
sendAt: string | null
createdAt: string
updatedAt: string
}
}
/** Delete Draft */
export interface DeleteDraftParams {
apiKey: string
inboxId: string
draftId: string
}
export interface DeleteDraftResult extends ToolResponse {
output: {
deleted: boolean
}
}
/** Send Draft */
export interface SendDraftParams {
apiKey: string
inboxId: string
draftId: string
}
export interface SendDraftResult extends ToolResponse {
output: {
messageId: string
threadId: string
}
}
/** List Drafts */
export interface ListDraftsParams {
apiKey: string
inboxId: string
limit?: number
pageToken?: string
}
export interface ListDraftsResult extends ToolResponse {
output: {
drafts: Array<{
draftId: string
inboxId: string
subject: string | null
to: string[]
cc: string[]
bcc: string[]
preview: string | null
sendStatus: string | null
sendAt: string | null
createdAt: string
updatedAt: string
}>
count: number
nextPageToken: string | null
}
}
/** Get Draft */
export interface GetDraftParams {
apiKey: string
inboxId: string
draftId: string
}
export interface GetDraftResult extends ToolResponse {
output: {
draftId: string
inboxId: string
subject: string | null
to: string[]
cc: string[]
bcc: string[]
text: string | null
html: string | null
preview: string | null
labels: string[]
inReplyTo: string | null
sendStatus: string | null
sendAt: string | null
createdAt: string
updatedAt: string
}
}
/** List Threads */
export interface ListThreadsParams {
apiKey: string
inboxId: string
limit?: number
pageToken?: string
labels?: string
before?: string
after?: string
}
export interface ListThreadsResult extends ToolResponse {
output: {
threads: Array<{
threadId: string
subject: string | null
senders: string[]
recipients: string[]
messageCount: number
lastMessageAt: string | null
createdAt: string
updatedAt: string
}>
count: number
nextPageToken: string | null
}
}
/** Get Thread */
export interface GetThreadParams {
apiKey: string
inboxId: string
threadId: string
}
export interface GetThreadResult extends ToolResponse {
output: {
threadId: string
subject: string | null
senders: string[]
recipients: string[]
messageCount: number
labels: string[]
lastMessageAt: string | null
createdAt: string
updatedAt: string
messages: Array<{
messageId: string
from: string | null
to: string[]
cc: string[]
bcc: string[]
subject: string | null
text: string | null
html: string | null
labels: string[]
timestamp: string | null
createdAt: string
}>
}
}
/** Update Thread Labels */
export interface UpdateThreadParams {
apiKey: string
inboxId: string
threadId: string
addLabels?: string
removeLabels?: string
}
export interface UpdateThreadResult extends ToolResponse {
output: {
threadId: string
labels: string[]
}
}
/** Delete Thread */
export interface DeleteThreadParams {
apiKey: string
inboxId: string
threadId: string
permanent?: boolean
}
export interface DeleteThreadResult extends ToolResponse {
output: {
deleted: boolean
}
}
/** List Messages */
export interface ListMessagesParams {
apiKey: string
inboxId: string
limit?: number
pageToken?: string
}
export interface ListMessagesResult extends ToolResponse {
output: {
messages: Array<{
messageId: string
from: string | null
to: string[]
subject: string | null
preview: string | null
timestamp: string | null
createdAt: string
}>
count: number
nextPageToken: string | null
}
}
/** Get Message */
export interface GetMessageParams {
apiKey: string
inboxId: string
messageId: string
}
export interface GetMessageResult extends ToolResponse {
output: {
messageId: string
threadId: string
from: string | null
to: string[]
cc: string[]
bcc: string[]
subject: string | null
text: string | null
html: string | null
labels: string[]
timestamp: string | null
createdAt: string
}
}
+164
View File
@@ -0,0 +1,164 @@
import type { UpdateDraftParams, UpdateDraftResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailUpdateDraftTool: ToolConfig<UpdateDraftParams, UpdateDraftResult> = {
id: 'agentmail_update_draft',
name: 'Update Draft',
description: 'Update an existing email draft in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the draft',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to update',
},
to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Recipient email addresses (comma-separated)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Draft subject line',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Plain text draft body',
},
html: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTML draft body',
},
cc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CC recipient email addresses (comma-separated)',
},
bcc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'BCC recipient email addresses (comma-separated)',
},
sendAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO 8601 timestamp to schedule sending',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/drafts/${params.draftId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.to) body.to = params.to.split(',').map((e) => e.trim())
if (params.subject) body.subject = params.subject
if (params.text) body.text = params.text
if (params.html) body.html = params.html
if (params.cc) body.cc = params.cc.split(',').map((e) => e.trim())
if (params.bcc) body.bcc = params.bcc.split(',').map((e) => e.trim())
if (params.sendAt) body.send_at = params.sendAt
return body
},
},
transformResponse: async (response): Promise<UpdateDraftResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to update draft',
output: {
draftId: '',
inboxId: '',
subject: null,
to: [],
cc: [],
bcc: [],
text: null,
html: null,
preview: null,
labels: [],
inReplyTo: null,
sendStatus: null,
sendAt: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
draftId: data.draft_id ?? '',
inboxId: data.inbox_id ?? '',
subject: data.subject ?? null,
to: data.to ?? [],
cc: data.cc ?? [],
bcc: data.bcc ?? [],
text: data.text ?? null,
html: data.html ?? null,
preview: data.preview ?? null,
labels: data.labels ?? [],
inReplyTo: data.in_reply_to ?? null,
sendStatus: data.send_status ?? null,
sendAt: data.send_at ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
draftId: { type: 'string', description: 'Unique identifier for the draft' },
inboxId: { type: 'string', description: 'Inbox the draft belongs to' },
subject: { type: 'string', description: 'Draft subject', optional: true },
to: { type: 'array', description: 'Recipient email addresses' },
cc: { type: 'array', description: 'CC email addresses' },
bcc: { type: 'array', description: 'BCC email addresses' },
text: { type: 'string', description: 'Plain text content', optional: true },
html: { type: 'string', description: 'HTML content', optional: true },
preview: { type: 'string', description: 'Draft preview text', optional: true },
labels: { type: 'array', description: 'Labels assigned to the draft' },
inReplyTo: { type: 'string', description: 'Message ID this draft replies to', optional: true },
sendStatus: {
type: 'string',
description: 'Send status (scheduled, sending, failed)',
optional: true,
},
sendAt: { type: 'string', description: 'Scheduled send time', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
+79
View File
@@ -0,0 +1,79 @@
import type { UpdateInboxParams, UpdateInboxResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailUpdateInboxTool: ToolConfig<UpdateInboxParams, UpdateInboxResult> = {
id: 'agentmail_update_inbox',
name: 'Update Inbox',
description: 'Update the display name of an email inbox in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox to update',
},
displayName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New display name for the inbox',
},
},
request: {
url: (params) => `https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
display_name: params.displayName,
}),
},
transformResponse: async (response): Promise<UpdateInboxResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to update inbox',
output: {
inboxId: '',
email: '',
displayName: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
inboxId: data.inbox_id ?? '',
email: data.email ?? '',
displayName: data.display_name ?? null,
createdAt: data.created_at ?? '',
updatedAt: data.updated_at ?? '',
},
}
},
outputs: {
inboxId: { type: 'string', description: 'Unique identifier for the inbox' },
email: { type: 'string', description: 'Email address of the inbox' },
displayName: { type: 'string', description: 'Display name of the inbox', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp' },
updatedAt: { type: 'string', description: 'Last updated timestamp' },
},
}
@@ -0,0 +1,87 @@
import type { UpdateMessageParams, UpdateMessageResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailUpdateMessageTool: ToolConfig<UpdateMessageParams, UpdateMessageResult> = {
id: 'agentmail_update_message',
name: 'Update Message',
description: 'Add or remove labels on an email message in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the message',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the message to update',
},
addLabels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated labels to add to the message',
},
removeLabels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated labels to remove from the message',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/messages/${params.messageId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.addLabels) {
body.add_labels = params.addLabels.split(',').map((l) => l.trim())
}
if (params.removeLabels) {
body.remove_labels = params.removeLabels.split(',').map((l) => l.trim())
}
return body
},
},
transformResponse: async (response): Promise<UpdateMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to update message',
output: { messageId: '', labels: [] },
}
}
return {
success: true,
output: {
messageId: data.message_id ?? '',
labels: data.labels ?? [],
},
}
},
outputs: {
messageId: { type: 'string', description: 'Unique identifier for the message' },
labels: { type: 'array', description: 'Current labels on the message' },
},
}
+85
View File
@@ -0,0 +1,85 @@
import type { UpdateThreadParams, UpdateThreadResult } from '@/tools/agentmail/types'
import type { ToolConfig } from '@/tools/types'
export const agentmailUpdateThreadTool: ToolConfig<UpdateThreadParams, UpdateThreadResult> = {
id: 'agentmail_update_thread',
name: 'Update Thread Labels',
description: 'Add or remove labels on an email thread in AgentMail',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentMail API key',
},
inboxId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the inbox containing the thread',
},
threadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the thread to update',
},
addLabels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated labels to add to the thread',
},
removeLabels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated labels to remove from the thread',
},
},
request: {
url: (params) =>
`https://api.agentmail.to/v0/inboxes/${params.inboxId.trim()}/threads/${params.threadId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
...(params.addLabels && {
add_labels: params.addLabels.split(',').map((l) => l.trim()),
}),
...(params.removeLabels && {
remove_labels: params.removeLabels.split(',').map((l) => l.trim()),
}),
}),
},
transformResponse: async (response): Promise<UpdateThreadResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data.message ?? 'Failed to update thread',
output: { threadId: '', labels: [] },
}
}
return {
success: true,
output: {
threadId: data.thread_id ?? '',
labels: data.labels ?? [],
},
}
},
outputs: {
threadId: { type: 'string', description: 'Unique identifier for the thread' },
labels: { type: 'array', description: 'Current labels on the thread' },
},
}
+132
View File
@@ -0,0 +1,132 @@
import type {
AgentPhoneCreateCallParams,
AgentPhoneCreateCallResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneCreateCallTool: ToolConfig<
AgentPhoneCreateCallParams,
AgentPhoneCreateCallResult
> = {
id: 'agentphone_create_call',
name: 'Create Outbound Call',
description: 'Initiate an outbound voice call from an AgentPhone agent',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
agentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Agent that will handle the call',
},
toNumber: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Phone number to call in E.164 format (e.g. +14155551234)',
},
fromNumberId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent's first assigned number is used.",
},
initialGreeting: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional greeting spoken when the recipient answers',
},
voice: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: "Voice ID override for this call (defaults to the agent's configured voice)",
},
systemPrompt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook',
},
},
request: {
url: 'https://api.agentphone.to/v1/calls',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
agentId: params.agentId,
toNumber: params.toNumber,
}
if (params.fromNumberId) body.fromNumberId = params.fromNumberId
if (params.initialGreeting) body.initialGreeting = params.initialGreeting
if (params.voice) body.voice = params.voice
if (params.systemPrompt) body.systemPrompt = params.systemPrompt
return body
},
},
transformResponse: async (response): Promise<AgentPhoneCreateCallResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create call',
output: {
id: '',
agentId: null,
status: null,
toNumber: null,
fromNumber: null,
phoneNumberId: null,
direction: null,
startedAt: null,
},
}
}
return {
success: true,
output: {
id: data.id ?? data.callId ?? '',
agentId: data.agentId ?? null,
status: data.status ?? null,
toNumber: data.toNumber ?? null,
fromNumber: data.fromNumber ?? null,
phoneNumberId: data.phoneNumberId ?? null,
direction: data.direction ?? null,
startedAt: data.startedAt ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Unique call identifier' },
agentId: { type: 'string', description: 'Agent handling the call', optional: true },
status: { type: 'string', description: 'Initial call status', optional: true },
toNumber: { type: 'string', description: 'Destination phone number', optional: true },
fromNumber: { type: 'string', description: 'Caller ID used for the call', optional: true },
phoneNumberId: {
type: 'string',
description: 'ID of the phone number used as caller ID',
optional: true,
},
direction: { type: 'string', description: 'Call direction (outbound)', optional: true },
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
},
}
+109
View File
@@ -0,0 +1,109 @@
import type {
AgentPhoneCreateContactParams,
AgentPhoneCreateContactResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneCreateContactTool: ToolConfig<
AgentPhoneCreateContactParams,
AgentPhoneCreateContactResult
> = {
id: 'agentphone_create_contact',
name: 'Create Contact',
description: 'Create a new contact in AgentPhone',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
phoneNumber: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Phone number in E.164 format (e.g. +14155551234)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: "Contact's full name",
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: "Contact's email address",
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Freeform notes stored on the contact',
},
},
request: {
url: 'https://api.agentphone.to/v1/contacts',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
phoneNumber: params.phoneNumber,
name: params.name,
}
if (params.email) body.email = params.email
if (params.notes) body.notes = params.notes
return body
},
},
transformResponse: async (response): Promise<AgentPhoneCreateContactResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create contact',
output: {
id: '',
phoneNumber: '',
name: '',
email: null,
notes: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
id: data.id ?? '',
phoneNumber: data.phoneNumber ?? '',
name: data.name ?? '',
email: data.email ?? null,
notes: data.notes ?? null,
createdAt: data.createdAt ?? '',
updatedAt: data.updatedAt ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Contact ID' },
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
name: { type: 'string', description: 'Contact name' },
email: { type: 'string', description: 'Contact email address', optional: true },
notes: { type: 'string', description: 'Freeform notes', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
},
}
+106
View File
@@ -0,0 +1,106 @@
import type {
AgentPhoneCreateNumberParams,
AgentPhoneCreateNumberResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneCreateNumberTool: ToolConfig<
AgentPhoneCreateNumberParams,
AgentPhoneCreateNumberResult
> = {
id: 'agentphone_create_number',
name: 'Create Phone Number',
description: 'Provision a new SMS- and voice-enabled phone number',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Two-letter country code (e.g. US, CA). Defaults to US.',
},
areaCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Preferred area code (US/CA only, e.g. "415"). Best-effort — may be ignored if unavailable.',
},
agentId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optionally attach the number to an agent immediately',
},
},
request: {
url: 'https://api.agentphone.to/v1/numbers',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.country) body.country = params.country
if (params.areaCode) body.areaCode = params.areaCode
if (params.agentId) body.agentId = params.agentId
return body
},
},
transformResponse: async (response): Promise<AgentPhoneCreateNumberResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to create phone number',
output: {
id: '',
phoneNumber: '',
country: '',
status: '',
type: '',
agentId: null,
createdAt: '',
},
}
}
return {
success: true,
output: {
id: data.id ?? '',
phoneNumber: data.phoneNumber ?? '',
country: data.country ?? '',
status: data.status ?? '',
type: data.type ?? '',
agentId: data.agentId ?? null,
createdAt: data.createdAt ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Unique phone number ID' },
phoneNumber: { type: 'string', description: 'Provisioned phone number in E.164 format' },
country: { type: 'string', description: 'Two-letter country code' },
status: { type: 'string', description: 'Number status (e.g. active)' },
type: { type: 'string', description: 'Number type (e.g. sms)', optional: true },
agentId: {
type: 'string',
description: 'Agent the number is attached to',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 timestamp when the number was created' },
},
}
@@ -0,0 +1,67 @@
import type {
AgentPhoneDeleteContactParams,
AgentPhoneDeleteContactResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneDeleteContactTool: ToolConfig<
AgentPhoneDeleteContactParams,
AgentPhoneDeleteContactResult
> = {
id: 'agentphone_delete_contact',
name: 'Delete Contact',
description: 'Delete a contact by ID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
contactId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Contact ID',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params): Promise<AgentPhoneDeleteContactResult> => {
const contactId = params?.contactId?.trim() ?? ''
if (!response.ok) {
let errorMessage = 'Failed to delete contact'
try {
const data = await response.json()
errorMessage = data?.detail?.[0]?.msg ?? data?.message ?? errorMessage
} catch {
// Response body may be empty; ignore parse failures.
}
return {
success: false,
error: errorMessage,
output: { id: contactId, deleted: false },
}
}
return {
success: true,
output: { id: contactId, deleted: true },
}
},
outputs: {
id: { type: 'string', description: 'ID of the deleted contact' },
deleted: { type: 'boolean', description: 'Whether the contact was deleted successfully' },
},
}
+146
View File
@@ -0,0 +1,146 @@
import type {
AgentPhoneGetCallParams,
AgentPhoneGetCallResult,
AgentPhoneTranscriptTurn,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetCallTool: ToolConfig<AgentPhoneGetCallParams, AgentPhoneGetCallResult> = {
id: 'agentphone_get_call',
name: 'Get Call',
description: 'Fetch a call and its full transcript',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
callId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the call to retrieve',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/calls/${params.callId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetCallResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch call',
output: {
id: '',
agentId: null,
phoneNumberId: null,
phoneNumber: null,
fromNumber: '',
toNumber: '',
direction: '',
status: '',
startedAt: null,
endedAt: null,
durationSeconds: null,
lastTranscriptSnippet: null,
recordingUrl: null,
recordingAvailable: null,
transcripts: [],
},
}
}
const transcripts: AgentPhoneTranscriptTurn[] = (data.transcripts ?? []).map(
(turn: Record<string, unknown>) => ({
id: (turn.id as string) ?? '',
transcript: (turn.transcript as string) ?? '',
confidence: (turn.confidence as number | null) ?? null,
response: (turn.response as string | null) ?? null,
createdAt: (turn.createdAt as string) ?? '',
})
)
return {
success: true,
output: {
id: data.id ?? '',
agentId: data.agentId ?? null,
phoneNumberId: data.phoneNumberId ?? null,
phoneNumber: data.phoneNumber ?? null,
fromNumber: data.fromNumber ?? '',
toNumber: data.toNumber ?? '',
direction: data.direction ?? '',
status: data.status ?? '',
startedAt: data.startedAt ?? null,
endedAt: data.endedAt ?? null,
durationSeconds: data.durationSeconds ?? null,
lastTranscriptSnippet: data.lastTranscriptSnippet ?? null,
recordingUrl: data.recordingUrl ?? null,
recordingAvailable: data.recordingAvailable ?? null,
transcripts,
},
}
},
outputs: {
id: { type: 'string', description: 'Call ID' },
agentId: { type: 'string', description: 'Agent that handled the call', optional: true },
phoneNumberId: { type: 'string', description: 'Phone number ID', optional: true },
phoneNumber: {
type: 'string',
description: 'Phone number used for the call',
optional: true,
},
fromNumber: { type: 'string', description: 'Caller phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
direction: { type: 'string', description: 'inbound or outbound', optional: true },
status: { type: 'string', description: 'Call status' },
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
endedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
durationSeconds: { type: 'number', description: 'Call duration in seconds', optional: true },
lastTranscriptSnippet: {
type: 'string',
description: 'Last transcript snippet',
optional: true,
},
recordingUrl: { type: 'string', description: 'Recording audio URL', optional: true },
recordingAvailable: {
type: 'boolean',
description: 'Whether a recording is available',
optional: true,
},
transcripts: {
type: 'array',
description: 'Ordered transcript turns for the call',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Transcript turn ID' },
transcript: { type: 'string', description: 'User utterance' },
confidence: {
type: 'number',
description: 'Speech recognition confidence',
optional: true,
},
response: {
type: 'string',
description: 'Agent response (when available)',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
},
}
@@ -0,0 +1,94 @@
import type {
AgentPhoneGetCallTranscriptParams,
AgentPhoneGetCallTranscriptResult,
AgentPhoneTranscriptEntry,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetCallTranscriptTool: ToolConfig<
AgentPhoneGetCallTranscriptParams,
AgentPhoneGetCallTranscriptResult
> = {
id: 'agentphone_get_call_transcript',
name: 'Get Call Transcript',
description: 'Get the full ordered transcript for a call',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
callId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the call to retrieve the transcript for',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/calls/${params.callId.trim()}/transcript`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params): Promise<AgentPhoneGetCallTranscriptResult> => {
const data = await response.json()
const callId = params?.callId?.trim() ?? ''
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch transcript',
output: { callId, transcript: [] },
}
}
const rawTurns = Array.isArray(data?.transcript)
? data.transcript
: Array.isArray(data)
? data
: []
const transcript: AgentPhoneTranscriptEntry[] = rawTurns.map(
(turn: Record<string, unknown>) => ({
role: (turn.role as string) ?? '',
content: (turn.content as string) ?? '',
createdAt: (turn.createdAt as string) ?? (turn.created_at as string) ?? null,
})
)
return {
success: true,
output: { callId: data?.callId ?? callId, transcript },
}
},
outputs: {
callId: { type: 'string', description: 'Call ID' },
transcript: {
type: 'array',
description: 'Ordered transcript turns for the call',
items: {
type: 'object',
properties: {
role: {
type: 'string',
description: 'Speaker role (user or agent)',
},
content: { type: 'string', description: 'Turn content' },
createdAt: {
type: 'string',
description: 'ISO 8601 timestamp',
optional: true,
},
},
},
},
},
}
+81
View File
@@ -0,0 +1,81 @@
import type {
AgentPhoneGetContactParams,
AgentPhoneGetContactResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetContactTool: ToolConfig<
AgentPhoneGetContactParams,
AgentPhoneGetContactResult
> = {
id: 'agentphone_get_contact',
name: 'Get Contact',
description: 'Fetch a single contact by ID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
contactId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Contact ID',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetContactResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch contact',
output: {
id: '',
phoneNumber: '',
name: '',
email: null,
notes: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
id: data.id ?? '',
phoneNumber: data.phoneNumber ?? '',
name: data.name ?? '',
email: data.email ?? null,
notes: data.notes ?? null,
createdAt: data.createdAt ?? '',
updatedAt: data.updatedAt ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Contact ID' },
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
name: { type: 'string', description: 'Contact name' },
email: { type: 'string', description: 'Contact email address', optional: true },
notes: { type: 'string', description: 'Freeform notes', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
},
}
@@ -0,0 +1,143 @@
import type {
AgentPhoneConversationMessage,
AgentPhoneGetConversationParams,
AgentPhoneGetConversationResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetConversationTool: ToolConfig<
AgentPhoneGetConversationParams,
AgentPhoneGetConversationResult
> = {
id: 'agentphone_get_conversation',
name: 'Get Conversation',
description: 'Get a conversation along with its recent messages',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
conversationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Conversation ID',
},
messageLimit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of recent messages to include (default 50, max 100)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.messageLimit === 'number') {
query.set('message_limit', String(params.messageLimit))
}
const qs = query.toString()
return `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetConversationResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch conversation',
output: {
id: '',
agentId: null,
phoneNumberId: '',
phoneNumber: '',
participant: '',
lastMessageAt: '',
messageCount: 0,
metadata: null,
createdAt: '',
messages: [],
},
}
}
const messages: AgentPhoneConversationMessage[] = (data.messages ?? []).map(
(msg: Record<string, unknown>) => ({
id: (msg.id as string) ?? '',
body: (msg.body as string) ?? '',
fromNumber: (msg.fromNumber as string) ?? '',
toNumber: (msg.toNumber as string) ?? '',
direction: (msg.direction as string) ?? '',
channel: (msg.channel as string | null) ?? null,
mediaUrl: (msg.mediaUrl as string | null) ?? null,
mediaUrls: Array.isArray(msg.mediaUrls) ? (msg.mediaUrls as string[]) : [],
receivedAt: (msg.receivedAt as string) ?? '',
})
)
return {
success: true,
output: {
id: data.id ?? '',
agentId: data.agentId ?? null,
phoneNumberId: data.phoneNumberId ?? '',
phoneNumber: data.phoneNumber ?? '',
participant: data.participant ?? '',
lastMessageAt: data.lastMessageAt ?? '',
messageCount: data.messageCount ?? 0,
metadata: data.metadata ?? null,
createdAt: data.createdAt ?? '',
messages,
},
}
},
outputs: {
id: { type: 'string', description: 'Conversation ID' },
agentId: { type: 'string', description: 'Agent ID', optional: true },
phoneNumberId: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
participant: { type: 'string', description: 'External participant phone number' },
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
messageCount: { type: 'number', description: 'Number of messages in the conversation' },
metadata: {
type: 'json',
description: 'Custom metadata stored on the conversation',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
messages: {
type: 'array',
description: 'Recent messages in the conversation',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Message ID' },
body: { type: 'string', description: 'Message text' },
fromNumber: { type: 'string', description: 'Sender phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
direction: { type: 'string', description: 'inbound or outbound' },
channel: { type: 'string', description: 'sms, mms, or imessage', optional: true },
mediaUrl: { type: 'string', description: 'Attached media URL', optional: true },
mediaUrls: {
type: 'array',
description: 'All attached media URLs',
items: { type: 'string' },
},
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
},
}
@@ -0,0 +1,122 @@
import type {
AgentPhoneConversationMessage,
AgentPhoneGetConversationMessagesParams,
AgentPhoneGetConversationMessagesResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetConversationMessagesTool: ToolConfig<
AgentPhoneGetConversationMessagesParams,
AgentPhoneGetConversationMessagesResult
> = {
id: 'agentphone_get_conversation_messages',
name: 'Get Conversation Messages',
description: 'Get paginated messages for a conversation',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
conversationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Conversation ID',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of messages to return (default 50, max 200)',
},
before: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return messages received before this ISO 8601 timestamp',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return messages received after this ISO 8601 timestamp',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (params.before) query.set('before', params.before)
if (params.after) query.set('after', params.after)
const qs = query.toString()
return `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}/messages${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetConversationMessagesResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch messages',
output: { data: [], hasMore: false },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(msg: Record<string, unknown>): AgentPhoneConversationMessage => ({
id: (msg.id as string) ?? '',
body: (msg.body as string) ?? '',
fromNumber: (msg.fromNumber as string) ?? '',
toNumber: (msg.toNumber as string) ?? '',
direction: (msg.direction as string) ?? '',
channel: (msg.channel as string | null) ?? null,
mediaUrl: (msg.mediaUrl as string | null) ?? null,
mediaUrls: Array.isArray(msg.mediaUrls) ? (msg.mediaUrls as string[]) : [],
receivedAt: (msg.receivedAt as string) ?? '',
})
),
hasMore: data.hasMore ?? false,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Messages in the conversation',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Message ID' },
body: { type: 'string', description: 'Message text' },
fromNumber: { type: 'string', description: 'Sender phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
direction: { type: 'string', description: 'inbound or outbound' },
channel: { type: 'string', description: 'sms, mms, or imessage', optional: true },
mediaUrl: { type: 'string', description: 'Attached media URL', optional: true },
mediaUrls: {
type: 'array',
description: 'All attached media URLs',
items: { type: 'string' },
},
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
hasMore: { type: 'boolean', description: 'Whether more messages are available' },
},
}
@@ -0,0 +1,115 @@
import type {
AgentPhoneGetNumberMessagesParams,
AgentPhoneGetNumberMessagesResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetNumberMessagesTool: ToolConfig<
AgentPhoneGetNumberMessagesParams,
AgentPhoneGetNumberMessagesResult
> = {
id: 'agentphone_get_number_messages',
name: 'Get Phone Number Messages',
description: 'Fetch messages received on a specific phone number',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
numberId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the phone number',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of messages to return (default 50, max 200)',
},
before: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return messages received before this ISO 8601 timestamp',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return messages received after this ISO 8601 timestamp',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (params.before) query.set('before', params.before)
if (params.after) query.set('after', params.after)
const qs = query.toString()
return `https://api.agentphone.to/v1/numbers/${params.numberId.trim()}/messages${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetNumberMessagesResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch messages',
output: { data: [], hasMore: false },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map((msg: Record<string, unknown>) => ({
id: (msg.id as string) ?? '',
from_: (msg.from_ as string) ?? '',
to: (msg.to as string) ?? '',
body: (msg.body as string) ?? '',
direction: (msg.direction as string) ?? '',
channel: (msg.channel as string | null) ?? null,
receivedAt: (msg.receivedAt as string) ?? '',
})),
hasMore: data.hasMore ?? false,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Messages received on the number',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Message ID' },
from_: { type: 'string', description: 'Sender phone number (E.164)' },
to: { type: 'string', description: 'Recipient phone number (E.164)' },
body: { type: 'string', description: 'Message text' },
direction: { type: 'string', description: 'inbound or outbound' },
channel: {
type: 'string',
description: 'Channel (sms, mms, etc.)',
optional: true,
},
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
hasMore: { type: 'boolean', description: 'Whether more messages are available' },
},
}
+127
View File
@@ -0,0 +1,127 @@
import type { AgentPhoneGetUsageParams, AgentPhoneGetUsageResult } from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetUsageTool: ToolConfig<
AgentPhoneGetUsageParams,
AgentPhoneGetUsageResult
> = {
id: 'agentphone_get_usage',
name: 'Get Usage',
description: 'Retrieve current usage statistics for the AgentPhone account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
},
request: {
url: 'https://api.agentphone.to/v1/usage',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetUsageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch usage',
output: {
plan: {
name: '',
limits: {
numbers: null,
messagesPerMonth: null,
voiceMinutesPerMonth: null,
maxCallDurationMinutes: null,
concurrentCalls: null,
},
},
numbers: { used: null, limit: null, remaining: null },
stats: {
totalMessages: null,
messagesLast24h: null,
messagesLast7d: null,
messagesLast30d: null,
totalCalls: null,
callsLast24h: null,
callsLast7d: null,
callsLast30d: null,
totalWebhookDeliveries: null,
successfulWebhookDeliveries: null,
failedWebhookDeliveries: null,
},
periodStart: '',
periodEnd: '',
},
}
}
const planLimits = data?.plan?.limits ?? {}
const numbers = data?.numbers ?? {}
const stats = data?.stats ?? {}
return {
success: true,
output: {
plan: {
name: data?.plan?.name ?? '',
limits: {
numbers: planLimits.numbers ?? null,
messagesPerMonth: planLimits.messagesPerMonth ?? null,
voiceMinutesPerMonth: planLimits.voiceMinutesPerMonth ?? null,
maxCallDurationMinutes: planLimits.maxCallDurationMinutes ?? null,
concurrentCalls: planLimits.concurrentCalls ?? null,
},
},
numbers: {
used: numbers.used ?? null,
limit: numbers.limit ?? null,
remaining: numbers.remaining ?? null,
},
stats: {
totalMessages: stats.totalMessages ?? null,
messagesLast24h: stats.messagesLast24h ?? null,
messagesLast7d: stats.messagesLast7d ?? null,
messagesLast30d: stats.messagesLast30d ?? null,
totalCalls: stats.totalCalls ?? null,
callsLast24h: stats.callsLast24h ?? null,
callsLast7d: stats.callsLast7d ?? null,
callsLast30d: stats.callsLast30d ?? null,
totalWebhookDeliveries: stats.totalWebhookDeliveries ?? null,
successfulWebhookDeliveries: stats.successfulWebhookDeliveries ?? null,
failedWebhookDeliveries: stats.failedWebhookDeliveries ?? null,
},
periodStart: data.periodStart ?? '',
periodEnd: data.periodEnd ?? '',
},
}
},
outputs: {
plan: {
type: 'json',
description:
'Plan name and limits (name, limits: numbers/messagesPerMonth/voiceMinutesPerMonth/maxCallDurationMinutes/concurrentCalls)',
},
numbers: {
type: 'json',
description: 'Phone number usage (used, limit, remaining)',
},
stats: {
type: 'json',
description:
'Usage stats: totalMessages, messagesLast24h/7d/30d, totalCalls, callsLast24h/7d/30d, totalWebhookDeliveries, successfulWebhookDeliveries, failedWebhookDeliveries',
},
periodStart: { type: 'string', description: 'Billing period start' },
periodEnd: { type: 'string', description: 'Billing period end' },
},
}
@@ -0,0 +1,88 @@
import type {
AgentPhoneGetUsageDailyParams,
AgentPhoneGetUsageDailyResult,
AgentPhoneUsageDailyEntry,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetUsageDailyTool: ToolConfig<
AgentPhoneGetUsageDailyParams,
AgentPhoneGetUsageDailyResult
> = {
id: 'agentphone_get_usage_daily',
name: 'Get Daily Usage',
description: 'Get a daily breakdown of usage (messages, calls, webhooks) for the last N days',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
days: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of days to return (1-365, default 30)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.days === 'number') query.set('days', String(params.days))
const qs = query.toString()
return `https://api.agentphone.to/v1/usage/daily${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetUsageDailyResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch daily usage',
output: { data: [], days: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(entry: Record<string, unknown>): AgentPhoneUsageDailyEntry => ({
date: (entry.date as string) ?? '',
messages: (entry.messages as number) ?? 0,
calls: (entry.calls as number) ?? 0,
webhooks: (entry.webhooks as number) ?? 0,
})
),
days: data.days ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Daily usage entries',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Day (YYYY-MM-DD)' },
messages: { type: 'number', description: 'Messages that day' },
calls: { type: 'number', description: 'Calls that day' },
webhooks: { type: 'number', description: 'Webhook deliveries that day' },
},
},
},
days: { type: 'number', description: 'Number of days returned' },
},
}
@@ -0,0 +1,88 @@
import type {
AgentPhoneGetUsageMonthlyParams,
AgentPhoneGetUsageMonthlyResult,
AgentPhoneUsageMonthlyEntry,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneGetUsageMonthlyTool: ToolConfig<
AgentPhoneGetUsageMonthlyParams,
AgentPhoneGetUsageMonthlyResult
> = {
id: 'agentphone_get_usage_monthly',
name: 'Get Monthly Usage',
description: 'Get monthly usage aggregation (messages, calls, webhooks) for the last N months',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
months: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of months to return (1-24, default 6)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.months === 'number') query.set('months', String(params.months))
const qs = query.toString()
return `https://api.agentphone.to/v1/usage/monthly${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneGetUsageMonthlyResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to fetch monthly usage',
output: { data: [], months: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(entry: Record<string, unknown>): AgentPhoneUsageMonthlyEntry => ({
month: (entry.month as string) ?? '',
messages: (entry.messages as number) ?? 0,
calls: (entry.calls as number) ?? 0,
webhooks: (entry.webhooks as number) ?? 0,
})
),
months: data.months ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Monthly usage entries',
items: {
type: 'object',
properties: {
month: { type: 'string', description: 'Month (YYYY-MM)' },
messages: { type: 'number', description: 'Messages that month' },
calls: { type: 'number', description: 'Calls that month' },
webhooks: { type: 'number', description: 'Webhook deliveries that month' },
},
},
},
months: { type: 'number', description: 'Number of months returned' },
},
}
+23
View File
@@ -0,0 +1,23 @@
export { agentphoneCreateCallTool } from '@/tools/agentphone/create_call'
export { agentphoneCreateContactTool } from '@/tools/agentphone/create_contact'
export { agentphoneCreateNumberTool } from '@/tools/agentphone/create_number'
export { agentphoneDeleteContactTool } from '@/tools/agentphone/delete_contact'
export { agentphoneGetCallTool } from '@/tools/agentphone/get_call'
export { agentphoneGetCallTranscriptTool } from '@/tools/agentphone/get_call_transcript'
export { agentphoneGetContactTool } from '@/tools/agentphone/get_contact'
export { agentphoneGetConversationTool } from '@/tools/agentphone/get_conversation'
export { agentphoneGetConversationMessagesTool } from '@/tools/agentphone/get_conversation_messages'
export { agentphoneGetNumberMessagesTool } from '@/tools/agentphone/get_number_messages'
export { agentphoneGetUsageTool } from '@/tools/agentphone/get_usage'
export { agentphoneGetUsageDailyTool } from '@/tools/agentphone/get_usage_daily'
export { agentphoneGetUsageMonthlyTool } from '@/tools/agentphone/get_usage_monthly'
export { agentphoneListCallsTool } from '@/tools/agentphone/list_calls'
export { agentphoneListContactsTool } from '@/tools/agentphone/list_contacts'
export { agentphoneListConversationsTool } from '@/tools/agentphone/list_conversations'
export { agentphoneListNumbersTool } from '@/tools/agentphone/list_numbers'
export { agentphoneReactToMessageTool } from '@/tools/agentphone/react_to_message'
export { agentphoneReleaseNumberTool } from '@/tools/agentphone/release_number'
export { agentphoneSendMessageTool } from '@/tools/agentphone/send_message'
export * from '@/tools/agentphone/types'
export { agentphoneUpdateContactTool } from '@/tools/agentphone/update_contact'
export { agentphoneUpdateConversationTool } from '@/tools/agentphone/update_conversation'
+169
View File
@@ -0,0 +1,169 @@
import type {
AgentPhoneCallSummary,
AgentPhoneListCallsParams,
AgentPhoneListCallsResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneListCallsTool: ToolConfig<
AgentPhoneListCallsParams,
AgentPhoneListCallsResult
> = {
id: 'agentphone_list_calls',
name: 'List Calls',
description: 'List voice calls for this AgentPhone account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default 20, max 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip (min 0)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by status (completed, in-progress, failed)',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by direction (inbound, outbound)',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by call type (pstn, web)',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search by phone number (matches fromNumber or toNumber)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
if (params.status) query.set('status', params.status)
if (params.direction) query.set('direction', params.direction)
if (params.type) query.set('type', params.type)
if (params.search) query.set('search', params.search)
const qs = query.toString()
return `https://api.agentphone.to/v1/calls${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneListCallsResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list calls',
output: { data: [], hasMore: false, total: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(call: Record<string, unknown>): AgentPhoneCallSummary => ({
id: (call.id as string) ?? '',
agentId: (call.agentId as string | null) ?? null,
phoneNumberId: (call.phoneNumberId as string | null) ?? null,
phoneNumber: (call.phoneNumber as string | null) ?? null,
fromNumber: (call.fromNumber as string) ?? '',
toNumber: (call.toNumber as string) ?? '',
direction: (call.direction as string) ?? '',
status: (call.status as string) ?? '',
startedAt: (call.startedAt as string | null) ?? null,
endedAt: (call.endedAt as string | null) ?? null,
durationSeconds: (call.durationSeconds as number | null) ?? null,
lastTranscriptSnippet: (call.lastTranscriptSnippet as string | null) ?? null,
recordingUrl: (call.recordingUrl as string | null) ?? null,
recordingAvailable: (call.recordingAvailable as boolean | null) ?? null,
})
),
hasMore: data.hasMore ?? false,
total: data.total ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Calls',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Call ID' },
agentId: {
type: 'string',
description: 'Agent that handled the call',
optional: true,
},
phoneNumberId: {
type: 'string',
description: 'Phone number ID used for the call',
optional: true,
},
phoneNumber: {
type: 'string',
description: 'Phone number used for the call',
optional: true,
},
fromNumber: { type: 'string', description: 'Caller phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
direction: { type: 'string', description: 'inbound or outbound', optional: true },
status: { type: 'string', description: 'Call status' },
startedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
endedAt: { type: 'string', description: 'ISO 8601 timestamp', optional: true },
durationSeconds: {
type: 'number',
description: 'Call duration in seconds',
optional: true,
},
lastTranscriptSnippet: {
type: 'string',
description: 'Last transcript snippet',
optional: true,
},
recordingUrl: { type: 'string', description: 'Recording audio URL', optional: true },
recordingAvailable: {
type: 'boolean',
description: 'Whether a recording is available',
optional: true,
},
},
},
},
hasMore: { type: 'boolean', description: 'Whether more results are available' },
total: { type: 'number', description: 'Total number of matching calls' },
},
}
+110
View File
@@ -0,0 +1,110 @@
import type {
AgentPhoneContact,
AgentPhoneListContactsParams,
AgentPhoneListContactsResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneListContactsTool: ToolConfig<
AgentPhoneListContactsParams,
AgentPhoneListContactsResult
> = {
id: 'agentphone_list_contacts',
name: 'List Contacts',
description: 'List contacts for this AgentPhone account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by name or phone number (case-insensitive contains)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default 50, max 200)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip (min 0)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (params.search) query.set('search', params.search)
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
const qs = query.toString()
return `https://api.agentphone.to/v1/contacts${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneListContactsResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list contacts',
output: { data: [], hasMore: false, total: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(c: Record<string, unknown>): AgentPhoneContact => ({
id: (c.id as string) ?? '',
phoneNumber: (c.phoneNumber as string) ?? '',
name: (c.name as string) ?? '',
email: (c.email as string | null) ?? null,
notes: (c.notes as string | null) ?? null,
createdAt: (c.createdAt as string) ?? '',
updatedAt: (c.updatedAt as string) ?? '',
})
),
hasMore: data.hasMore ?? false,
total: data.total ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Contacts',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
name: { type: 'string', description: 'Contact name' },
email: { type: 'string', description: 'Contact email address', optional: true },
notes: { type: 'string', description: 'Freeform notes', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
},
},
},
hasMore: { type: 'boolean', description: 'Whether more results are available' },
total: { type: 'number', description: 'Total number of contacts' },
},
}
@@ -0,0 +1,113 @@
import type {
AgentPhoneConversationSummary,
AgentPhoneListConversationsParams,
AgentPhoneListConversationsResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneListConversationsTool: ToolConfig<
AgentPhoneListConversationsParams,
AgentPhoneListConversationsResult
> = {
id: 'agentphone_list_conversations',
name: 'List Conversations',
description: 'List conversations (message threads) for this AgentPhone account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default 20, max 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip (min 0)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
const qs = query.toString()
return `https://api.agentphone.to/v1/conversations${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneListConversationsResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list conversations',
output: { data: [], hasMore: false, total: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map(
(conv: Record<string, unknown>): AgentPhoneConversationSummary => ({
id: (conv.id as string) ?? '',
agentId: (conv.agentId as string | null) ?? null,
phoneNumberId: (conv.phoneNumberId as string) ?? '',
phoneNumber: (conv.phoneNumber as string) ?? '',
participant: (conv.participant as string) ?? '',
lastMessageAt: (conv.lastMessageAt as string) ?? '',
lastMessagePreview: (conv.lastMessagePreview as string) ?? '',
messageCount: (conv.messageCount as number) ?? 0,
metadata: (conv.metadata as Record<string, unknown> | null) ?? null,
createdAt: (conv.createdAt as string) ?? '',
})
),
hasMore: data.hasMore ?? false,
total: data.total ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Conversations',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Conversation ID' },
agentId: { type: 'string', description: 'Agent ID', optional: true },
phoneNumberId: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
participant: { type: 'string', description: 'External participant phone number' },
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
lastMessagePreview: { type: 'string', description: 'Last message preview' },
messageCount: { type: 'number', description: 'Number of messages in the conversation' },
metadata: {
type: 'json',
description: 'Custom metadata stored on the conversation',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
hasMore: { type: 'boolean', description: 'Whether more results are available' },
total: { type: 'number', description: 'Total number of conversations' },
},
}
+100
View File
@@ -0,0 +1,100 @@
import type {
AgentPhoneListNumbersParams,
AgentPhoneListNumbersResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneListNumbersTool: ToolConfig<
AgentPhoneListNumbersParams,
AgentPhoneListNumbersResult
> = {
id: 'agentphone_list_numbers',
name: 'List Phone Numbers',
description: 'List all phone numbers provisioned for this AgentPhone account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default 20, max 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip (min 0)',
},
},
request: {
url: (params) => {
const query = new URLSearchParams()
if (typeof params.limit === 'number') query.set('limit', String(params.limit))
if (typeof params.offset === 'number') query.set('offset', String(params.offset))
const qs = query.toString()
return `https://api.agentphone.to/v1/numbers${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response): Promise<AgentPhoneListNumbersResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to list phone numbers',
output: { data: [], hasMore: false, total: 0 },
}
}
return {
success: true,
output: {
data: (data.data ?? []).map((num: Record<string, unknown>) => ({
id: (num.id as string) ?? '',
phoneNumber: (num.phoneNumber as string) ?? '',
country: (num.country as string) ?? '',
status: (num.status as string) ?? '',
type: (num.type as string) ?? '',
agentId: (num.agentId as string | null) ?? null,
createdAt: (num.createdAt as string) ?? '',
})),
hasMore: data.hasMore ?? false,
total: data.total ?? 0,
},
}
},
outputs: {
data: {
type: 'array',
description: 'Phone numbers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
country: { type: 'string', description: 'Two-letter country code' },
status: { type: 'string', description: 'Number status' },
type: { type: 'string', description: 'Number type (e.g. sms)', optional: true },
agentId: { type: 'string', description: 'Attached agent ID', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
},
},
},
hasMore: { type: 'boolean', description: 'Whether more results are available' },
total: { type: 'number', description: 'Total number of phone numbers' },
},
}
@@ -0,0 +1,76 @@
import type {
AgentPhoneReactToMessageParams,
AgentPhoneReactToMessageResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneReactToMessageTool: ToolConfig<
AgentPhoneReactToMessageParams,
AgentPhoneReactToMessageResult
> = {
id: 'agentphone_react_to_message',
name: 'React to Message',
description: 'Send an iMessage tapback reaction to a message (iMessage only)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the message to react to',
},
reaction: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Reaction type: love, like, dislike, laugh, emphasize, or question',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/messages/${params.messageId.trim()}/reactions`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({ reaction: params.reaction }),
},
transformResponse: async (response, params): Promise<AgentPhoneReactToMessageResult> => {
const data = await response.json()
const messageId = params?.messageId?.trim() ?? ''
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to send reaction',
output: { id: '', reactionType: '', messageId, channel: '' },
}
}
return {
success: true,
output: {
id: data.id ?? '',
reactionType: data.reaction_type ?? '',
messageId: data.message_id ?? messageId,
channel: data.channel ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Reaction ID' },
reactionType: { type: 'string', description: 'Reaction type applied' },
messageId: { type: 'string', description: 'ID of the message that was reacted to' },
channel: { type: 'string', description: 'Channel (imessage)' },
},
}
@@ -0,0 +1,67 @@
import type {
AgentPhoneReleaseNumberParams,
AgentPhoneReleaseNumberResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneReleaseNumberTool: ToolConfig<
AgentPhoneReleaseNumberParams,
AgentPhoneReleaseNumberResult
> = {
id: 'agentphone_release_number',
name: 'Release Phone Number',
description: 'Release (delete) a phone number. This action is irreversible.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
numberId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the phone number to release',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/numbers/${params.numberId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params): Promise<AgentPhoneReleaseNumberResult> => {
const numberId = params?.numberId?.trim() ?? ''
if (!response.ok) {
let errorMessage = 'Failed to release phone number'
try {
const data = await response.json()
errorMessage = data?.detail?.[0]?.msg ?? data?.message ?? errorMessage
} catch {
// Response body may be empty on DELETE errors; ignore parse failures.
}
return {
success: false,
error: errorMessage,
output: { id: numberId, released: false },
}
}
return {
success: true,
output: { id: numberId, released: true },
}
},
outputs: {
id: { type: 'string', description: 'ID of the released phone number' },
released: { type: 'boolean', description: 'Whether the number was released successfully' },
},
}
+105
View File
@@ -0,0 +1,105 @@
import type {
AgentPhoneSendMessageParams,
AgentPhoneSendMessageResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneSendMessageTool: ToolConfig<
AgentPhoneSendMessageParams,
AgentPhoneSendMessageResult
> = {
id: 'agentphone_send_message',
name: 'Send Message',
description: 'Send an outbound SMS or iMessage from an AgentPhone agent',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
agentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Agent sending the message',
},
toNumber: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Recipient phone number in E.164 format (e.g. +14155551234)',
},
body: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Message text to send',
},
mediaUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional URL of an image, video, or file to attach',
},
numberId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"Phone number ID to send from. If omitted, the agent's first assigned number is used.",
},
},
request: {
url: 'https://api.agentphone.to/v1/messages',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
agent_id: params.agentId,
to_number: params.toNumber,
body: params.body,
}
if (params.mediaUrl) body.media_url = params.mediaUrl
if (params.numberId) body.number_id = params.numberId
return body
},
},
transformResponse: async (response): Promise<AgentPhoneSendMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to send message',
output: { id: '', status: '', channel: '', fromNumber: '', toNumber: '' },
}
}
return {
success: true,
output: {
id: data.id ?? '',
status: data.status ?? '',
channel: data.channel ?? '',
fromNumber: data.from_number ?? '',
toNumber: data.to_number ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Message ID' },
status: { type: 'string', description: 'Delivery status' },
channel: { type: 'string', description: 'sms, mms, or imessage' },
fromNumber: { type: 'string', description: 'Sender phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
},
}
+451
View File
@@ -0,0 +1,451 @@
import type { ToolResponse } from '@/tools/types'
interface AgentPhoneNumber {
id: string
phoneNumber: string
country: string
status: string
type: string
agentId: string | null
createdAt: string
}
interface AgentPhoneNumberMessage {
id: string
from_: string
to: string
body: string
direction: string
channel: string | null
receivedAt: string
}
export interface AgentPhoneConversationSummary {
id: string
agentId: string | null
phoneNumberId: string
phoneNumber: string
participant: string
lastMessageAt: string
lastMessagePreview: string
messageCount: number
metadata: Record<string, unknown> | null
createdAt: string
}
export interface AgentPhoneConversationMessage {
id: string
body: string
fromNumber: string
toNumber: string
direction: string
channel: string | null
mediaUrl: string | null
mediaUrls: string[]
receivedAt: string
}
interface AgentPhoneConversationDetail {
id: string
agentId: string | null
phoneNumberId: string
phoneNumber: string
participant: string
lastMessageAt: string
messageCount: number
metadata: Record<string, unknown> | null
createdAt: string
messages: AgentPhoneConversationMessage[]
}
export interface AgentPhoneCallSummary {
id: string
agentId: string | null
phoneNumberId: string | null
phoneNumber: string | null
fromNumber: string
toNumber: string
direction: string
status: string
startedAt: string | null
endedAt: string | null
durationSeconds: number | null
lastTranscriptSnippet: string | null
recordingUrl: string | null
recordingAvailable: boolean | null
}
export interface AgentPhoneTranscriptTurn {
id: string
transcript: string
confidence: number | null
response: string | null
createdAt: string
}
export interface AgentPhoneTranscriptEntry {
role: string
content: string
createdAt: string | null
}
interface AgentPhoneCallDetail extends AgentPhoneCallSummary {
transcripts: AgentPhoneTranscriptTurn[]
}
export interface AgentPhoneContact {
id: string
phoneNumber: string
name: string
email: string | null
notes: string | null
createdAt: string
updatedAt: string
}
export interface AgentPhoneCreateNumberParams {
apiKey: string
country?: string
areaCode?: string
agentId?: string
}
export interface AgentPhoneCreateNumberResult extends ToolResponse {
output: AgentPhoneNumber
}
export interface AgentPhoneListNumbersParams {
apiKey: string
limit?: number
offset?: number
}
export interface AgentPhoneListNumbersResult extends ToolResponse {
output: {
data: AgentPhoneNumber[]
hasMore: boolean
total: number
}
}
export interface AgentPhoneReleaseNumberParams {
apiKey: string
numberId: string
}
export interface AgentPhoneReleaseNumberResult extends ToolResponse {
output: {
id: string
released: boolean
}
}
export interface AgentPhoneGetNumberMessagesParams {
apiKey: string
numberId: string
limit?: number
before?: string
after?: string
}
export interface AgentPhoneGetNumberMessagesResult extends ToolResponse {
output: {
data: AgentPhoneNumberMessage[]
hasMore: boolean
}
}
export interface AgentPhoneCreateCallParams {
apiKey: string
agentId: string
toNumber: string
fromNumberId?: string
initialGreeting?: string
voice?: string
systemPrompt?: string
}
export interface AgentPhoneCreateCallResult extends ToolResponse {
output: {
id: string
agentId: string | null
status: string | null
toNumber: string | null
fromNumber: string | null
phoneNumberId: string | null
direction: string | null
startedAt: string | null
}
}
export interface AgentPhoneListCallsParams {
apiKey: string
limit?: number
offset?: number
status?: string
direction?: string
type?: string
search?: string
}
export interface AgentPhoneListCallsResult extends ToolResponse {
output: {
data: AgentPhoneCallSummary[]
hasMore: boolean
total: number
}
}
export interface AgentPhoneGetCallParams {
apiKey: string
callId: string
}
export interface AgentPhoneGetCallResult extends ToolResponse {
output: AgentPhoneCallDetail
}
export interface AgentPhoneGetCallTranscriptParams {
apiKey: string
callId: string
}
export interface AgentPhoneGetCallTranscriptResult extends ToolResponse {
output: {
callId: string
transcript: AgentPhoneTranscriptEntry[]
}
}
export interface AgentPhoneListConversationsParams {
apiKey: string
limit?: number
offset?: number
}
export interface AgentPhoneListConversationsResult extends ToolResponse {
output: {
data: AgentPhoneConversationSummary[]
hasMore: boolean
total: number
}
}
export interface AgentPhoneGetConversationParams {
apiKey: string
conversationId: string
messageLimit?: number
}
export interface AgentPhoneGetConversationResult extends ToolResponse {
output: AgentPhoneConversationDetail
}
export interface AgentPhoneUpdateConversationParams {
apiKey: string
conversationId: string
metadata?: Record<string, unknown> | null
}
export interface AgentPhoneUpdateConversationResult extends ToolResponse {
output: AgentPhoneConversationDetail
}
export interface AgentPhoneGetConversationMessagesParams {
apiKey: string
conversationId: string
limit?: number
before?: string
after?: string
}
export interface AgentPhoneGetConversationMessagesResult extends ToolResponse {
output: {
data: AgentPhoneConversationMessage[]
hasMore: boolean
}
}
export interface AgentPhoneSendMessageParams {
apiKey: string
agentId: string
toNumber: string
body: string
mediaUrl?: string
numberId?: string
}
export interface AgentPhoneSendMessageResult extends ToolResponse {
output: {
id: string
status: string
channel: string
fromNumber: string
toNumber: string
}
}
export type AgentPhoneReactionType =
| 'love'
| 'like'
| 'dislike'
| 'laugh'
| 'emphasize'
| 'question'
export interface AgentPhoneReactToMessageParams {
apiKey: string
messageId: string
reaction: AgentPhoneReactionType
}
export interface AgentPhoneReactToMessageResult extends ToolResponse {
output: {
id: string
reactionType: string
messageId: string
channel: string
}
}
export interface AgentPhoneCreateContactParams {
apiKey: string
phoneNumber: string
name: string
email?: string
notes?: string
}
export interface AgentPhoneCreateContactResult extends ToolResponse {
output: AgentPhoneContact
}
export interface AgentPhoneListContactsParams {
apiKey: string
search?: string
limit?: number
offset?: number
}
export interface AgentPhoneListContactsResult extends ToolResponse {
output: {
data: AgentPhoneContact[]
hasMore: boolean
total: number
}
}
export interface AgentPhoneGetContactParams {
apiKey: string
contactId: string
}
export interface AgentPhoneGetContactResult extends ToolResponse {
output: AgentPhoneContact
}
export interface AgentPhoneUpdateContactParams {
apiKey: string
contactId: string
phoneNumber?: string
name?: string
email?: string
notes?: string
}
export interface AgentPhoneUpdateContactResult extends ToolResponse {
output: AgentPhoneContact
}
export interface AgentPhoneDeleteContactParams {
apiKey: string
contactId: string
}
export interface AgentPhoneDeleteContactResult extends ToolResponse {
output: {
id: string
deleted: boolean
}
}
interface AgentPhoneUsagePlan {
name: string
limits: {
numbers: number | null
messagesPerMonth: number | null
voiceMinutesPerMonth: number | null
maxCallDurationMinutes: number | null
concurrentCalls: number | null
}
}
interface AgentPhoneUsageStats {
totalMessages: number | null
messagesLast24h: number | null
messagesLast7d: number | null
messagesLast30d: number | null
totalCalls: number | null
callsLast24h: number | null
callsLast7d: number | null
callsLast30d: number | null
totalWebhookDeliveries: number | null
successfulWebhookDeliveries: number | null
failedWebhookDeliveries: number | null
}
export interface AgentPhoneGetUsageParams {
apiKey: string
}
export interface AgentPhoneGetUsageResult extends ToolResponse {
output: {
plan: AgentPhoneUsagePlan
numbers: {
used: number | null
limit: number | null
remaining: number | null
}
stats: AgentPhoneUsageStats
periodStart: string
periodEnd: string
}
}
export interface AgentPhoneUsageDailyEntry {
date: string
messages: number
calls: number
webhooks: number
}
export interface AgentPhoneGetUsageDailyParams {
apiKey: string
days?: number
}
export interface AgentPhoneGetUsageDailyResult extends ToolResponse {
output: {
data: AgentPhoneUsageDailyEntry[]
days: number
}
}
export interface AgentPhoneUsageMonthlyEntry {
month: string
messages: number
calls: number
webhooks: number
}
export interface AgentPhoneGetUsageMonthlyParams {
apiKey: string
months?: number
}
export interface AgentPhoneGetUsageMonthlyResult extends ToolResponse {
output: {
data: AgentPhoneUsageMonthlyEntry[]
months: number
}
}
+114
View File
@@ -0,0 +1,114 @@
import type {
AgentPhoneUpdateContactParams,
AgentPhoneUpdateContactResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneUpdateContactTool: ToolConfig<
AgentPhoneUpdateContactParams,
AgentPhoneUpdateContactResult
> = {
id: 'agentphone_update_contact',
name: 'Update Contact',
description: "Update a contact's fields",
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
contactId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Contact ID',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New phone number in E.164 format',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New contact name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New email address',
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New freeform notes',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/contacts/${params.contactId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
if (params.name) body.name = params.name
if (params.email) body.email = params.email
if (params.notes) body.notes = params.notes
return body
},
},
transformResponse: async (response): Promise<AgentPhoneUpdateContactResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to update contact',
output: {
id: '',
phoneNumber: '',
name: '',
email: null,
notes: null,
createdAt: '',
updatedAt: '',
},
}
}
return {
success: true,
output: {
id: data.id ?? '',
phoneNumber: data.phoneNumber ?? '',
name: data.name ?? '',
email: data.email ?? null,
notes: data.notes ?? null,
createdAt: data.createdAt ?? '',
updatedAt: data.updatedAt ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Contact ID' },
phoneNumber: { type: 'string', description: 'Phone number in E.164 format' },
name: { type: 'string', description: 'Contact name' },
email: { type: 'string', description: 'Contact email address', optional: true },
notes: { type: 'string', description: 'Freeform notes', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' },
},
}
@@ -0,0 +1,152 @@
import type {
AgentPhoneConversationMessage,
AgentPhoneUpdateConversationParams,
AgentPhoneUpdateConversationResult,
} from '@/tools/agentphone/types'
import type { ToolConfig } from '@/tools/types'
export const agentphoneUpdateConversationTool: ToolConfig<
AgentPhoneUpdateConversationParams,
AgentPhoneUpdateConversationResult
> = {
id: 'agentphone_update_conversation',
name: 'Update Conversation',
description: 'Update conversation metadata (stored state). Pass null to clear existing metadata.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AgentPhone API key',
},
conversationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Conversation ID',
},
metadata: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Custom key-value metadata to store on the conversation. Pass null to clear existing metadata.',
},
},
request: {
url: (params) => `https://api.agentphone.to/v1/conversations/${params.conversationId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.metadata !== undefined) body.metadata = params.metadata
return body
},
},
transformResponse: async (response): Promise<AgentPhoneUpdateConversationResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: data?.detail?.[0]?.msg ?? data?.message ?? 'Failed to update conversation',
output: {
id: '',
agentId: null,
phoneNumberId: '',
phoneNumber: '',
participant: '',
lastMessageAt: '',
messageCount: 0,
metadata: null,
createdAt: '',
messages: [],
},
}
}
const rawMessages = Array.isArray(data?.messages) ? data.messages : []
const messages: AgentPhoneConversationMessage[] = rawMessages.map(
(message: Record<string, unknown>) => ({
id: (message.id as string) ?? '',
body: (message.body as string) ?? '',
fromNumber: (message.fromNumber as string) ?? '',
toNumber: (message.toNumber as string) ?? '',
direction: (message.direction as string) ?? '',
channel: (message.channel as string | null) ?? null,
mediaUrl: (message.mediaUrl as string | null) ?? null,
mediaUrls: Array.isArray(message.mediaUrls) ? (message.mediaUrls as string[]) : [],
receivedAt: (message.receivedAt as string) ?? '',
})
)
return {
success: true,
output: {
id: data.id ?? '',
agentId: data.agentId ?? null,
phoneNumberId: data.phoneNumberId ?? '',
phoneNumber: data.phoneNumber ?? '',
participant: data.participant ?? '',
lastMessageAt: data.lastMessageAt ?? '',
messageCount: data.messageCount ?? 0,
metadata: data.metadata ?? null,
createdAt: data.createdAt ?? '',
messages,
},
}
},
outputs: {
id: { type: 'string', description: 'Conversation ID' },
agentId: { type: 'string', description: 'Agent ID', optional: true },
phoneNumberId: { type: 'string', description: 'Phone number ID' },
phoneNumber: { type: 'string', description: 'Phone number' },
participant: { type: 'string', description: 'External participant phone number' },
lastMessageAt: { type: 'string', description: 'ISO 8601 timestamp' },
messageCount: { type: 'number', description: 'Number of messages' },
metadata: {
type: 'json',
description: 'Custom metadata stored on the conversation',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 timestamp' },
messages: {
type: 'array',
description: 'Messages in the conversation',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Message ID' },
body: { type: 'string', description: 'Message body' },
fromNumber: { type: 'string', description: 'Sender phone number' },
toNumber: { type: 'string', description: 'Recipient phone number' },
direction: { type: 'string', description: 'inbound or outbound' },
channel: {
type: 'string',
description: 'Channel (sms, mms, etc.)',
optional: true,
},
mediaUrl: {
type: 'string',
description: 'Media URL if any',
optional: true,
},
mediaUrls: {
type: 'array',
description: 'All attached media URLs',
items: { type: 'string' },
},
receivedAt: { type: 'string', description: 'ISO 8601 timestamp' },
},
},
},
},
}
+132
View File
@@ -0,0 +1,132 @@
import type { AgiloftAttachFileParams, AgiloftAttachFileResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftAttachFileTool: ToolConfig<AgiloftAttachFileParams, AgiloftAttachFileResponse> =
{
id: 'agiloft_attach_file',
name: 'Agiloft Attach File',
description: 'Attach a file to a field in an Agiloft record.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to attach the file to',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the attachment field',
},
file: {
type: 'file',
required: false,
visibility: 'user-or-llm',
description: 'File to attach',
},
fileName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Name to assign to the file (defaults to original file name)',
},
},
request: {
url: '/api/tools/agiloft/attach',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
fieldName: params.fieldName,
file: params.file,
fileName: params.fileName,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
recordId: '',
fieldName: '',
fileName: '',
totalAttachments: 0,
},
error: data.error || 'Failed to attach file',
}
}
return {
success: true,
output: {
recordId: data.output?.recordId ?? '',
fieldName: data.output?.fieldName ?? '',
fileName: data.output?.fileName ?? '',
totalAttachments: data.output?.totalAttachments ?? 0,
},
}
},
outputs: {
recordId: {
type: 'string',
description: 'ID of the record the file was attached to',
},
fieldName: {
type: 'string',
description: 'Name of the field the file was attached to',
},
fileName: {
type: 'string',
description: 'Name of the attached file',
},
totalAttachments: {
type: 'number',
description: 'Total number of files attached in the field after the operation',
},
},
}
+106
View File
@@ -0,0 +1,106 @@
import type {
AgiloftAttachmentInfoParams,
AgiloftAttachmentInfoResponse,
} from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftAttachmentInfoTool: ToolConfig<
AgiloftAttachmentInfoParams,
AgiloftAttachmentInfoResponse
> = {
id: 'agiloft_attachment_info',
name: 'Agiloft Attachment Info',
description: 'Get information about file attachments on a record field.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to check attachments on',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the attachment field to inspect',
},
},
request: {
url: () => '/api/tools/agiloft/attachment_info',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
fieldName: params.fieldName,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
attachments: {
type: 'array',
description: 'List of attachments with position, name, and size',
items: {
type: 'object',
properties: {
position: {
type: 'number',
description: 'Position index of the attachment in the field',
},
name: { type: 'string', description: 'File name of the attachment' },
size: { type: 'number', description: 'File size in bytes' },
},
},
},
totalCount: {
type: 'number',
description: 'Total number of attachments in the field',
},
},
}
+84
View File
@@ -0,0 +1,84 @@
import type { AgiloftCreateRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftCreateRecordTool: ToolConfig<AgiloftCreateRecordParams, AgiloftRecordResponse> =
{
id: 'agiloft_create_record',
name: 'Agiloft Create Record',
description: 'Create a new record in an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts", "contacts.employees")',
},
data: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Record field values as a JSON object (e.g., {"first_name": "John", "status": "Active"})',
},
},
request: {
url: () => '/api/tools/agiloft/create_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
data: params.data,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'string',
description: 'ID of the created record',
},
fields: {
type: 'json',
description: 'Field values of the created record',
},
},
}
+83
View File
@@ -0,0 +1,83 @@
import type { AgiloftDeleteRecordParams, AgiloftDeleteResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftDeleteRecordTool: ToolConfig<AgiloftDeleteRecordParams, AgiloftDeleteResponse> =
{
id: 'agiloft_delete_record',
name: 'Agiloft Delete Record',
description: 'Delete a record from an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts", "contacts.employees")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to delete',
},
},
request: {
url: () => '/api/tools/agiloft/delete_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'string',
description: 'ID of the deleted record',
},
deleted: {
type: 'boolean',
description: 'Whether the record was successfully deleted',
},
},
}
@@ -0,0 +1,93 @@
import type {
AgiloftGetChoiceLineIdParams,
AgiloftGetChoiceLineIdResponse,
} from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftGetChoiceLineIdTool: ToolConfig<
AgiloftGetChoiceLineIdParams,
AgiloftGetChoiceLineIdResponse
> = {
id: 'agiloft_get_choice_line_id',
name: 'Agiloft Get Choice Line ID',
description:
'Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "case", "contracts")',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Choice field name (e.g., "priority", "status")',
},
value: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Choice display value to resolve (e.g., "High", "Active")',
},
},
request: {
url: () => '/api/tools/agiloft/get_choice_line_id',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
fieldName: params.fieldName,
value: params.value,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
choiceLineId: {
type: 'number',
description: 'Internal numeric line ID of the choice value',
optional: true,
},
},
}
+14
View File
@@ -0,0 +1,14 @@
export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file'
export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info'
export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record'
export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record'
export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id'
export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record'
export { agiloftReadRecordTool } from '@/tools/agiloft/read_record'
export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment'
export { agiloftRetrieveAttachmentTool } from '@/tools/agiloft/retrieve_attachment'
export { agiloftSavedSearchTool } from '@/tools/agiloft/saved_search'
export { agiloftSearchRecordsTool } from '@/tools/agiloft/search_records'
export { agiloftSelectRecordsTool } from '@/tools/agiloft/select_records'
export * from '@/tools/agiloft/types'
export { agiloftUpdateRecordTool } from '@/tools/agiloft/update_record'
+99
View File
@@ -0,0 +1,99 @@
import type { AgiloftLockRecordParams, AgiloftLockResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftLockRecordTool: ToolConfig<AgiloftLockRecordParams, AgiloftLockResponse> = {
id: 'agiloft_lock_record',
name: 'Agiloft Lock Record',
description: 'Lock, unlock, or check the lock status of an Agiloft record.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to lock, unlock, or check',
},
lockAction: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "lock", "unlock", or "check"',
},
},
request: {
url: () => '/api/tools/agiloft/lock_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
lockAction: params.lockAction,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'string',
description: 'Record ID',
},
lockStatus: {
type: 'string',
description: 'Lock status (e.g., "LOCKED", "UNLOCKED")',
},
lockedBy: {
type: 'string',
description: 'Username of the user who locked the record',
optional: true,
},
lockExpiresInMinutes: {
type: 'number',
description: 'Minutes until the lock expires',
optional: true,
},
},
}
+89
View File
@@ -0,0 +1,89 @@
import type { AgiloftReadRecordParams, AgiloftRecordResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftReadRecordTool: ToolConfig<AgiloftReadRecordParams, AgiloftRecordResponse> = {
id: 'agiloft_read_record',
name: 'Agiloft Read Record',
description: 'Read a record by ID from an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts", "contacts.employees")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to read',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field names to include in the response',
},
},
request: {
url: () => '/api/tools/agiloft/read_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
fields: params.fields,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'string',
description: 'ID of the record',
},
fields: {
type: 'json',
description: 'Field values of the record',
},
},
}
+106
View File
@@ -0,0 +1,106 @@
import type {
AgiloftRemoveAttachmentParams,
AgiloftRemoveAttachmentResponse,
} from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftRemoveAttachmentTool: ToolConfig<
AgiloftRemoveAttachmentParams,
AgiloftRemoveAttachmentResponse
> = {
id: 'agiloft_remove_attachment',
name: 'Agiloft Remove Attachment',
description: 'Remove an attached file from a field in an Agiloft record.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record containing the attachment',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the attachment field',
},
position: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Position index of the file to remove (starting from 0)',
},
},
request: {
url: () => '/api/tools/agiloft/remove_attachment',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
fieldName: params.fieldName,
position: params.position,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
recordId: {
type: 'string',
description: 'ID of the record',
},
fieldName: {
type: 'string',
description: 'Name of the attachment field',
},
remainingAttachments: {
type: 'number',
description: 'Number of attachments remaining in the field after removal',
},
},
}
@@ -0,0 +1,117 @@
import type {
AgiloftRetrieveAttachmentParams,
AgiloftRetrieveAttachmentResponse,
} from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftRetrieveAttachmentTool: ToolConfig<
AgiloftRetrieveAttachmentParams,
AgiloftRetrieveAttachmentResponse
> = {
id: 'agiloft_retrieve_attachment',
name: 'Agiloft Retrieve Attachment',
description: 'Download an attached file from an Agiloft record field.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record containing the attachment',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the attachment field',
},
position: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Position index of the file in the field (starting from 0)',
},
},
request: {
url: '/api/tools/agiloft/retrieve',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
fieldName: params.fieldName,
position: params.position,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
file: { name: '', mimeType: '', data: '', size: 0 },
},
error: data.error || 'Failed to retrieve attachment',
}
}
return {
success: true,
output: {
file: {
name: data.output.file.name,
mimeType: data.output.file.mimeType,
data: data.output.file.data,
size: data.output.file.size,
},
},
}
},
outputs: {
file: {
type: 'file',
description: 'Downloaded attachment file',
},
},
}
+87
View File
@@ -0,0 +1,87 @@
import type { AgiloftSavedSearchParams, AgiloftSavedSearchResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftSavedSearchTool: ToolConfig<
AgiloftSavedSearchParams,
AgiloftSavedSearchResponse
> = {
id: 'agiloft_saved_search',
name: 'Agiloft Saved Search',
description: 'List saved searches defined for an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to list saved searches for (e.g., "contracts")',
},
},
request: {
url: () => '/api/tools/agiloft/saved_search',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
searches: {
type: 'array',
description: 'List of saved searches for the table',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Saved search name' },
label: { type: 'string', description: 'Saved search display label' },
id: { type: 'number', description: 'Saved search database identifier' },
description: {
type: 'string',
description: 'Saved search description',
optional: true,
},
},
},
},
},
}
+115
View File
@@ -0,0 +1,115 @@
import type { AgiloftSearchRecordsParams, AgiloftSearchResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftSearchRecordsTool: ToolConfig<
AgiloftSearchRecordsParams,
AgiloftSearchResponse
> = {
id: 'agiloft_search_records',
name: 'Agiloft Search Records',
description: 'Search for records in an Agiloft table using a query.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to search in (e.g., "contracts", "contacts.employees")',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Search query using Agiloft query syntax (e.g., "status=\'Active\'" or "company_name~=\'Acme\'")',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field names to include in the results',
},
page: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number for paginated results (starting from 0)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of records to return per page',
},
},
request: {
url: () => '/api/tools/agiloft/search_records',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
query: params.query,
fields: params.fields,
page: params.page,
limit: params.limit,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
records: {
type: 'json',
description: 'Array of matching records with their field values',
},
totalCount: {
type: 'number',
description: 'Total number of matching records',
},
page: {
type: 'number',
description: 'Current page number',
},
limit: {
type: 'number',
description: 'Records per page',
},
},
}
+89
View File
@@ -0,0 +1,89 @@
import type { AgiloftSelectRecordsParams, AgiloftSelectResponse } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftSelectRecordsTool: ToolConfig<
AgiloftSelectRecordsParams,
AgiloftSelectResponse
> = {
id: 'agiloft_select_records',
name: 'Agiloft Select Records',
description: 'Select record IDs matching a SQL WHERE clause from an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts", "contacts.employees")',
},
where: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'SQL WHERE clause using database column names (e.g., "summary like \'%new%\'" or "assigned_person=\'John Doe\'")',
},
},
request: {
url: () => '/api/tools/agiloft/select_records',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
where: params.where,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
recordIds: {
type: 'array',
description: 'Array of record IDs matching the query',
items: {
type: 'string',
},
},
totalCount: {
type: 'number',
description: 'Total number of matching records',
},
},
}
+173
View File
@@ -0,0 +1,173 @@
import type { ToolResponse } from '@/tools/types'
/**
* Base parameters shared by all Agiloft tools.
* Agiloft authenticates via instance URL, KB name, and user credentials.
*/
export interface AgiloftBaseParams {
instanceUrl: string
knowledgeBase: string
login: string
password: string
table: string
}
export interface AgiloftCreateRecordParams extends AgiloftBaseParams {
data: string
}
export interface AgiloftReadRecordParams extends AgiloftBaseParams {
recordId: string
fields?: string
}
export interface AgiloftUpdateRecordParams extends AgiloftBaseParams {
recordId: string
data: string
}
export interface AgiloftDeleteRecordParams extends AgiloftBaseParams {
recordId: string
}
export interface AgiloftSearchRecordsParams extends AgiloftBaseParams {
query: string
fields?: string
page?: string
limit?: string
}
export interface AgiloftSelectRecordsParams extends AgiloftBaseParams {
where: string
}
export type AgiloftSavedSearchParams = AgiloftBaseParams
export interface AgiloftAttachmentInfoParams extends AgiloftBaseParams {
recordId: string
fieldName: string
}
export interface AgiloftLockRecordParams extends AgiloftBaseParams {
recordId: string
lockAction: 'lock' | 'unlock' | 'check'
}
export interface AgiloftRecordResponse extends ToolResponse {
output: {
id: string | null
fields: Record<string, unknown>
}
}
export interface AgiloftDeleteResponse extends ToolResponse {
output: {
id: string
deleted: boolean
}
}
export interface AgiloftSearchResponse extends ToolResponse {
output: {
records: Record<string, unknown>[]
totalCount: number
page: number
limit: number
}
}
export interface AgiloftSelectResponse extends ToolResponse {
output: {
recordIds: string[]
totalCount: number
}
}
export interface AgiloftSavedSearchResponse extends ToolResponse {
output: {
searches: Array<{
name: string
label: string
id: string | number
description: string | null
}>
}
}
export interface AgiloftAttachmentInfoResponse extends ToolResponse {
output: {
attachments: Array<{
position: number
name: string
size: number
}>
totalCount: number
}
}
export interface AgiloftLockResponse extends ToolResponse {
output: {
id: string
lockStatus: string
lockedBy: string | null
lockExpiresInMinutes: number | null
}
}
export interface AgiloftAttachFileParams extends AgiloftBaseParams {
recordId: string
fieldName: string
file?: unknown
fileName?: string
}
export interface AgiloftAttachFileResponse extends ToolResponse {
output: {
recordId: string
fieldName: string
fileName: string
totalAttachments: number
}
}
export interface AgiloftRetrieveAttachmentParams extends AgiloftBaseParams {
recordId: string
fieldName: string
position: string
}
export interface AgiloftRetrieveAttachmentResponse extends ToolResponse {
output: {
file: {
name: string
mimeType: string
data: string
size: number
}
}
}
export interface AgiloftRemoveAttachmentParams extends AgiloftBaseParams {
recordId: string
fieldName: string
position: string
}
export interface AgiloftRemoveAttachmentResponse extends ToolResponse {
output: {
recordId: string
fieldName: string
remainingAttachments: number
}
}
export interface AgiloftGetChoiceLineIdParams extends AgiloftBaseParams {
fieldName: string
value: string
}
export interface AgiloftGetChoiceLineIdResponse extends ToolResponse {
output: {
choiceLineId: number | null
}
}
+91
View File
@@ -0,0 +1,91 @@
import type { AgiloftRecordResponse, AgiloftUpdateRecordParams } from '@/tools/agiloft/types'
import type { ToolConfig } from '@/tools/types'
export const agiloftUpdateRecordTool: ToolConfig<AgiloftUpdateRecordParams, AgiloftRecordResponse> =
{
id: 'agiloft_update_record',
name: 'Agiloft Update Record',
description: 'Update an existing record in an Agiloft table.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "contracts", "contacts.employees")',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the record to update',
},
data: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Updated field values as a JSON object (e.g., {"status": "Active", "priority": "High"})',
},
},
request: {
url: () => '/api/tools/agiloft/update_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
instanceUrl: params.instanceUrl,
knowledgeBase: params.knowledgeBase,
login: params.login,
password: params.password,
table: params.table,
recordId: params.recordId,
data: params.data,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output,
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'string',
description: 'ID of the updated record',
},
fields: {
type: 'json',
description: 'Updated field values of the record',
},
},
}
+158
View File
@@ -0,0 +1,158 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockValidateUrlWithDNS, mockSecureFetch } = vi.hoisted(() => ({
mockValidateUrlWithDNS: vi.fn(),
mockSecureFetch: vi.fn(),
}))
vi.mock('@/lib/core/security/input-validation.server', () => ({
validateUrlWithDNS: mockValidateUrlWithDNS,
secureFetchWithPinnedIP: mockSecureFetch,
}))
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
const baseParams = {
instanceUrl: 'https://example.agiloft.com',
knowledgeBase: 'demo',
login: 'admin',
password: 'secret',
table: 'contracts',
}
function mockResponse(body: { ok?: boolean; status?: number; json?: unknown; text?: string }) {
return {
ok: body.ok ?? true,
status: body.status ?? 200,
statusText: '',
headers: { get: () => null, getSetCookie: () => [], toRecord: () => ({}) },
body: null,
text: async () => body.text ?? '',
json: async () => body.json ?? {},
arrayBuffer: async () => new ArrayBuffer(0),
}
}
beforeEach(() => {
mockValidateUrlWithDNS.mockReset()
mockSecureFetch.mockReset()
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' })
})
describe('executeAgiloftRequest', () => {
it('resolves DNS once, logs in, runs the operation with the bearer token, then logs out — all pinned', async () => {
mockSecureFetch
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-1' } }))
.mockResolvedValueOnce(mockResponse({ json: { id: 42, fields: { name: 'foo' } } }))
.mockResolvedValueOnce(mockResponse({}))
const result = await executeAgiloftRequest(
baseParams,
(base) => ({
url: `${base}/ewws/REST/demo/contracts/42`,
method: 'GET',
headers: { Accept: 'application/json' },
}),
async (response) => {
const data = (await response.json()) as { id: number; fields: Record<string, unknown> }
return {
success: response.ok,
output: { id: String(data.id), fields: data.fields },
}
}
)
expect(result).toEqual({ success: true, output: { id: '42', fields: { name: 'foo' } } })
expect(mockValidateUrlWithDNS).toHaveBeenCalledWith(
'https://example.agiloft.com',
'instanceUrl'
)
const calls = mockSecureFetch.mock.calls
expect(calls).toHaveLength(3)
expect(calls[0][0]).toBe(
'https://example.agiloft.com/ewws/EWLogin?$KB=demo&$login=admin&$password=secret'
)
expect(calls[1][0]).toBe('https://example.agiloft.com/ewws/REST/demo/contracts/42')
expect(calls[2][0]).toBe('https://example.agiloft.com/ewws/EWLogout?$KB=demo')
for (const call of calls) {
expect(call[1]).toBe('203.0.113.10')
}
expect(calls[1][2]).toMatchObject({
method: 'GET',
headers: { Accept: 'application/json', Authorization: 'Bearer tok-1' },
})
})
it('still logs out when the operation throws', async () => {
mockSecureFetch
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-2' } }))
.mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
.mockResolvedValueOnce(mockResponse({}))
await expect(
executeAgiloftRequest(
baseParams,
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
async (response) => {
if (!response.ok) throw new Error('operation failed')
return { success: true, output: {} }
}
)
).rejects.toThrow('operation failed')
expect(mockSecureFetch).toHaveBeenCalledTimes(3)
expect(mockSecureFetch.mock.calls[2][0]).toContain('/ewws/EWLogout')
})
it('swallows logout failures (best-effort)', async () => {
mockSecureFetch
.mockResolvedValueOnce(mockResponse({ json: { access_token: 'tok-3' } }))
.mockResolvedValueOnce(mockResponse({ json: { ok: true } }))
.mockRejectedValueOnce(new Error('logout network error'))
const result = await executeAgiloftRequest(
baseParams,
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
async () => ({ success: true, output: {} })
)
expect(result.success).toBe(true)
})
it('throws when login does not return an access token', async () => {
mockSecureFetch.mockResolvedValueOnce(mockResponse({ json: {} }))
await expect(
executeAgiloftRequest(
baseParams,
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
async () => ({ success: true, output: {} })
)
).rejects.toThrow('Agiloft login did not return an access token')
expect(mockSecureFetch).toHaveBeenCalledTimes(1)
})
it('rejects an instance URL that resolves to a blocked IP without issuing any request', async () => {
mockValidateUrlWithDNS.mockResolvedValue({
isValid: false,
error: 'instanceUrl resolves to a blocked IP address',
})
await expect(
executeAgiloftRequest(
{ ...baseParams, instanceUrl: 'https://internal.attacker.com' },
(base) => ({ url: `${base}/ewws/REST/demo/contracts/42`, method: 'GET' }),
async () => ({ success: true, output: {} })
)
).rejects.toThrow(/blocked IP address/)
expect(mockSecureFetch).not.toHaveBeenCalled()
})
})
+132
View File
@@ -0,0 +1,132 @@
import { createLogger } from '@sim/logger'
import {
type SecureFetchResponse,
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import type { AgiloftBaseParams } from '@/tools/agiloft/types'
import type { HttpMethod, ToolResponse } from '@/tools/types'
const logger = createLogger('AgiloftAuthServer')
interface AgiloftRequestConfig {
url: string
method: HttpMethod
headers?: Record<string, string>
body?: string
}
/**
* Validates the Agiloft instance URL and resolves its DNS once, returning the
* resolved IP so subsequent requests can pin to it. This prevents DNS-rebinding
* (TOCTOU) SSRF where the hostname could resolve to a private IP on a later
* lookup. Server-only — uses node:dns/promises.
*/
export async function resolveAgiloftInstance(instanceUrl: string): Promise<string> {
const validation = await validateUrlWithDNS(instanceUrl, 'instanceUrl')
if (!validation.isValid || !validation.resolvedIP) {
throw new Error(validation.error || 'Invalid Agiloft instance URL')
}
return validation.resolvedIP
}
/**
* DNS-pinned variant of agiloftLogin. Requires a pre-resolved IP so the
* connection cannot be steered to a different host between validation and
* the actual TCP connection.
*/
export async function agiloftLoginPinned(
params: AgiloftBaseParams,
resolvedIP: string
): Promise<string> {
const base = params.instanceUrl.replace(/\/$/, '')
const kb = encodeURIComponent(params.knowledgeBase)
const login = encodeURIComponent(params.login)
const password = encodeURIComponent(params.password)
const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}`
const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' })
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`)
}
const data = (await response.json()) as { access_token?: string }
const token = data.access_token
if (!token) {
throw new Error('Agiloft login did not return an access token')
}
return token
}
/**
* DNS-pinned variant of agiloftLogout. Best-effort — failures are logged but
* not thrown.
*/
export async function agiloftLogoutPinned(
instanceUrl: string,
knowledgeBase: string,
token: string,
resolvedIP: string
): Promise<void> {
try {
const base = instanceUrl.replace(/\/$/, '')
const kb = encodeURIComponent(knowledgeBase)
await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
} catch (error) {
logger.warn('Agiloft logout failed (best-effort)', { error })
}
}
/**
* Shared wrapper that handles the full Agiloft auth lifecycle behind the
* codebase's SSRF-safe fetch path. The instance URL is validated and resolved
* to a concrete IP once via `validateUrlWithDNS` (which rejects hostnames that
* resolve to private/reserved addresses), and every hop — login, the operation
* request, and logout — is issued through `secureFetchWithPinnedIP` so the
* connection is pinned to that validated IP. This defeats DNS-rebinding (TOCTOU)
* SSRF where a hostname could resolve to an internal address on a later lookup.
*
* 1. Validate + resolve the instance URL once.
* 2. Login to obtain a Bearer token.
* 3. Execute the operation request with the token.
* 4. Logout to clean up the session (best-effort).
*
* The `buildRequest` callback receives the base URL and returns the request
* config. The `transformResponse` callback converts the raw response into the
* tool's output format.
*
* Server-only — uses node:dns/promises and node:http(s) via the pinned fetch.
*/
export async function executeAgiloftRequest<R extends ToolResponse>(
params: AgiloftBaseParams,
buildRequest: (base: string) => AgiloftRequestConfig,
transformResponse: (response: SecureFetchResponse) => Promise<R>
): Promise<R> {
const resolvedIP = await resolveAgiloftInstance(params.instanceUrl)
const token = await agiloftLoginPinned(params, resolvedIP)
const base = params.instanceUrl.replace(/\/$/, '')
try {
const req = buildRequest(base)
const response = await secureFetchWithPinnedIP(req.url, resolvedIP, {
method: req.method,
headers: {
...req.headers,
Authorization: `Bearer ${token}`,
},
body: req.body,
})
return await transformResponse(response)
} finally {
await agiloftLogoutPinned(params.instanceUrl, params.knowledgeBase, token, resolvedIP)
}
}
export type { SecureFetchResponse }
+162
View File
@@ -0,0 +1,162 @@
import type {
AgiloftAttachmentInfoParams,
AgiloftBaseParams,
AgiloftDeleteRecordParams,
AgiloftGetChoiceLineIdParams,
AgiloftLockRecordParams,
AgiloftReadRecordParams,
AgiloftRemoveAttachmentParams,
AgiloftRetrieveAttachmentParams,
AgiloftSavedSearchParams,
AgiloftSearchRecordsParams,
AgiloftSelectRecordsParams,
} from '@/tools/agiloft/types'
import type { HttpMethod } from '@/tools/types'
/** URL builders (credential-free -- auth is via Bearer token header) */
function encodeTable(params: AgiloftBaseParams) {
return {
kb: encodeURIComponent(params.knowledgeBase),
table: encodeURIComponent(params.table),
}
}
export function buildCreateRecordUrl(base: string, params: AgiloftBaseParams): string {
const { kb, table } = encodeTable(params)
return `${base}/ewws/REST/${kb}/${table}?$lang=en`
}
export function buildReadRecordUrl(base: string, params: AgiloftReadRecordParams): string {
const { kb, table } = encodeTable(params)
const id = encodeURIComponent(params.recordId.trim())
let url = `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
if (params.fields) {
const fieldList = params.fields
.split(',')
.map((f) => f.trim())
.filter(Boolean)
for (const field of fieldList) {
url += `&$fields=${encodeURIComponent(field)}`
}
}
return url
}
export function buildUpdateRecordUrl(
base: string,
params: AgiloftBaseParams & { recordId: string }
): string {
const { kb, table } = encodeTable(params)
const id = encodeURIComponent(params.recordId.trim())
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
}
export function buildDeleteRecordUrl(base: string, params: AgiloftDeleteRecordParams): string {
const { kb, table } = encodeTable(params)
const id = encodeURIComponent(params.recordId.trim())
return `${base}/ewws/REST/${kb}/${table}/${id}?$lang=en`
}
function buildEwBaseQuery(params: AgiloftBaseParams): string {
const { kb, table } = encodeTable(params)
return `$KB=${kb}&$table=${table}&$lang=en`
}
export function buildSearchRecordsUrl(base: string, params: AgiloftSearchRecordsParams): string {
const query = encodeURIComponent(params.query)
let url = `${base}/ewws/EWSearch/.json?${buildEwBaseQuery(params)}&query=${query}`
if (params.fields) {
const fieldList = params.fields
.split(',')
.map((f) => f.trim())
.filter(Boolean)
for (const field of fieldList) {
url += `&field=${encodeURIComponent(field)}`
}
}
if (params.page) {
url += `&page=${encodeURIComponent(params.page)}`
}
if (params.limit) {
url += `&limit=${encodeURIComponent(params.limit)}`
}
return url
}
export function buildSelectRecordsUrl(base: string, params: AgiloftSelectRecordsParams): string {
const where = encodeURIComponent(params.where)
return `${base}/ewws/EWSelect/.json?${buildEwBaseQuery(params)}&where=${where}`
}
export function buildSavedSearchUrl(base: string, params: AgiloftSavedSearchParams): string {
return `${base}/ewws/EWSavedSearch/.json?${buildEwBaseQuery(params)}`
}
export function buildRetrieveAttachmentUrl(
base: string,
params: AgiloftRetrieveAttachmentParams
): string {
const id = encodeURIComponent(params.recordId.trim())
const field = encodeURIComponent(params.fieldName.trim())
const position = encodeURIComponent(params.position)
return `${base}/ewws/EWRetrieve?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
}
export function buildRemoveAttachmentUrl(
base: string,
params: AgiloftRemoveAttachmentParams
): string {
const id = encodeURIComponent(params.recordId.trim())
const field = encodeURIComponent(params.fieldName.trim())
const position = encodeURIComponent(params.position)
return `${base}/ewws/EWRemoveAttachment?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}`
}
export function buildAttachmentInfoUrl(base: string, params: AgiloftAttachmentInfoParams): string {
const id = encodeURIComponent(params.recordId.trim())
const fieldName = encodeURIComponent(params.fieldName.trim())
return `${base}/ewws/EWAttachInfo/.json?${buildEwBaseQuery(params)}&id=${id}&field=${fieldName}`
}
export function buildLockRecordUrl(base: string, params: AgiloftLockRecordParams): string {
const id = encodeURIComponent(params.recordId.trim())
return `${base}/ewws/EWLock/.json?${buildEwBaseQuery(params)}&id=${id}`
}
export function buildAttachFileUrl(
base: string,
params: AgiloftBaseParams & { recordId: string; fieldName: string },
fileName: string
): string {
const { kb, table } = encodeTable(params)
const recordId = encodeURIComponent(params.recordId.trim())
const fieldName = encodeURIComponent(params.fieldName.trim())
const encodedFileName = encodeURIComponent(fileName)
return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}`
}
export function buildGetChoiceLineIdUrl(
base: string,
params: AgiloftGetChoiceLineIdParams
): string {
const field = encodeURIComponent(params.fieldName.trim())
const value = encodeURIComponent(params.value.trim())
return `${base}/ewws/EWGetChoiceLineId/.json?${buildEwBaseQuery(params)}&field=${field}&value=${value}`
}
export function getLockHttpMethod(lockAction: string): HttpMethod {
switch (lockAction) {
case 'lock':
return 'PUT'
case 'unlock':
return 'DELETE'
default:
return 'GET'
}
}
+120
View File
@@ -0,0 +1,120 @@
import type { AhrefsAnchorsParams, AhrefsAnchorsResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'anchor,links_to_target,dofollow_links,refdomains,first_seen,last_seen'
export const anchorsTool: ToolConfig<AhrefsAnchorsParams, AhrefsAnchorsResponse> = {
id: 'ahrefs_anchors',
name: 'Ahrefs Anchors',
description:
"Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text.",
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
history: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Historical scope: "live" (currently live), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/anchors')
url.searchParams.set('target', params.target)
url.searchParams.set('select', SELECT_FIELDS)
if (params.mode) url.searchParams.set('mode', params.mode)
url.searchParams.set('history', params.history || 'all_time')
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get anchors')
}
const anchors = (data.anchors || []).map((item: any) => ({
anchor: item.anchor || '',
backlinks: item.links_to_target ?? 0,
dofollowBacklinks: item.dofollow_links ?? 0,
referringDomains: item.refdomains ?? 0,
firstSeen: item.first_seen || '',
lastSeen: item.last_seen ?? null,
}))
return {
success: true,
output: {
anchors,
},
}
},
outputs: {
anchors: {
type: 'array',
description: 'Anchor text distribution for the backlink profile',
items: {
type: 'object',
properties: {
anchor: { type: 'string', description: 'The anchor text' },
backlinks: { type: 'number', description: 'Total backlinks using this anchor text' },
dofollowBacklinks: {
type: 'number',
description: 'Number of dofollow backlinks using this anchor text',
},
referringDomains: {
type: 'number',
description: 'Number of unique referring domains using this anchor text',
},
firstSeen: {
type: 'string',
description: 'When a link with this anchor was first found',
},
lastSeen: {
type: 'string',
description: 'When a backlink with this anchor was last seen (null if still live)',
optional: true,
},
},
},
},
},
}
+113
View File
@@ -0,0 +1,113 @@
import type { AhrefsBacklinksParams, AhrefsBacklinksResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'url_from,url_to,anchor,domain_rating_source,is_dofollow,first_seen,last_visited'
export const backlinksTool: ToolConfig<AhrefsBacklinksParams, AhrefsBacklinksResponse> = {
id: 'ahrefs_backlinks',
name: 'Ahrefs Backlinks',
description:
'Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
history: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Historical scope: "live" (currently live backlinks), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date).',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/all-backlinks')
url.searchParams.set('target', params.target)
url.searchParams.set('select', SELECT_FIELDS)
if (params.mode) url.searchParams.set('mode', params.mode)
url.searchParams.set('history', params.history || 'all_time')
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get backlinks')
}
const backlinks = (data.backlinks || []).map((link: any) => ({
urlFrom: link.url_from || '',
urlTo: link.url_to || '',
anchor: link.anchor || '',
domainRatingSource: link.domain_rating_source ?? 0,
isDofollow: link.is_dofollow ?? false,
firstSeen: link.first_seen || '',
lastVisited: link.last_visited || '',
}))
return {
success: true,
output: {
backlinks,
},
}
},
outputs: {
backlinks: {
type: 'array',
description: 'List of backlinks pointing to the target',
items: {
type: 'object',
properties: {
urlFrom: { type: 'string', description: 'The URL of the page containing the backlink' },
urlTo: { type: 'string', description: 'The URL being linked to' },
anchor: { type: 'string', description: 'The anchor text of the link' },
domainRatingSource: {
type: 'number',
description: 'Domain Rating of the linking domain',
},
isDofollow: { type: 'boolean', description: 'Whether the link is dofollow' },
firstSeen: { type: 'string', description: 'When the backlink was first discovered' },
lastVisited: { type: 'string', description: 'When the backlink was last checked' },
},
},
},
},
}
+103
View File
@@ -0,0 +1,103 @@
import type { AhrefsBacklinksStatsParams, AhrefsBacklinksStatsResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const backlinksStatsTool: ToolConfig<
AhrefsBacklinksStatsParams,
AhrefsBacklinksStatsResponse
> = {
id: 'ahrefs_backlinks_stats',
name: 'Ahrefs Backlinks Stats',
description:
'Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks-stats')
url.searchParams.set('target', params.target)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
if (params.mode) url.searchParams.set('mode', params.mode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get backlinks stats')
}
const metrics = data.metrics || {}
return {
success: true,
output: {
stats: {
liveBacklinks: metrics.live ?? 0,
liveReferringDomains: metrics.live_refdomains ?? 0,
allTimeBacklinks: metrics.all_time ?? 0,
allTimeReferringDomains: metrics.all_time_refdomains ?? 0,
},
},
}
},
outputs: {
stats: {
type: 'object',
description: 'Backlink and referring domain totals',
properties: {
liveBacklinks: { type: 'number', description: 'Number of currently live backlinks' },
liveReferringDomains: {
type: 'number',
description: 'Number of currently live referring domains',
},
allTimeBacklinks: {
type: 'number',
description: 'Total backlinks ever discovered, including lost ones',
},
allTimeReferringDomains: {
type: 'number',
description: 'Total referring domains ever discovered, including lost ones',
},
},
},
},
}
+165
View File
@@ -0,0 +1,165 @@
import type { AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'url,domain_rating,ahrefs_rank,backlinks,refdomains,org_traffic,org_keywords,paid_traffic'
export const batchAnalysisTool: ToolConfig<AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse> =
{
id: 'ahrefs_batch_analysis',
name: 'Ahrefs Batch Analysis',
description:
'Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.',
version: '1.0.0',
params: {
targets: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of domains or URLs to analyze. Example: "example.com,competitor.com"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
protocol: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Protocol applied to every target: "both" (default), "http", or "https"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
},
volumeMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: () => 'https://api.ahrefs.com/v3/batch-analysis/batch-analysis',
method: 'POST',
headers: (params) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
const targets = params.targets
.split(',')
.map((target) => target.trim())
.filter((target) => target.length > 0)
.map((url) => ({
url,
mode: params.mode || 'subdomains',
protocol: params.protocol || 'both',
}))
return {
select: SELECT_FIELDS.split(','),
targets,
country: params.country || 'us',
volume_mode: params.volumeMode || 'monthly',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to run batch analysis')
}
const results = (data.targets || []).map((item: any) => ({
url: item.url || '',
index: item.index ?? 0,
domainRating: item.domain_rating ?? null,
ahrefsRank: item.ahrefs_rank ?? null,
backlinks: item.backlinks ?? null,
referringDomains: item.refdomains ?? null,
organicTraffic: item.org_traffic ?? null,
organicKeywords: item.org_keywords ?? null,
paidTraffic: item.paid_traffic ?? null,
error: item.error ?? null,
}))
return {
success: true,
output: {
results,
},
}
},
outputs: {
results: {
type: 'array',
description: 'Bulk metrics for each analyzed target, in submission order',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The analyzed target URL or domain' },
index: { type: 'number', description: 'Index of the target in the submitted list' },
domainRating: {
type: 'number',
description: 'Domain Rating score (0-100)',
optional: true,
},
ahrefsRank: {
type: 'number',
description: 'Ahrefs Rank (global ranking)',
optional: true,
},
backlinks: {
type: 'number',
description: 'Total backlinks to the target',
optional: true,
},
referringDomains: {
type: 'number',
description: 'Unique domains linking to the target',
optional: true,
},
organicTraffic: {
type: 'number',
description: 'Estimated monthly organic traffic',
optional: true,
},
organicKeywords: {
type: 'number',
description: 'Number of organic keywords ranked (top 100)',
optional: true,
},
paidTraffic: {
type: 'number',
description: 'Estimated monthly paid search traffic',
optional: true,
},
error: {
type: 'string',
description: 'Error message if this target could not be analyzed',
optional: true,
},
},
},
},
},
}
+113
View File
@@ -0,0 +1,113 @@
import type {
AhrefsBrokenBacklinksParams,
AhrefsBrokenBacklinksResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'url_from,url_to,http_code_target,anchor,domain_rating_source'
export const brokenBacklinksTool: ToolConfig<
AhrefsBrokenBacklinksParams,
AhrefsBrokenBacklinksResponse
> = {
id: 'ahrefs_broken_backlinks',
name: 'Ahrefs Broken Backlinks',
description:
'Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/broken-backlinks')
url.searchParams.set('target', params.target)
url.searchParams.set('select', SELECT_FIELDS)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get broken backlinks')
}
const brokenBacklinks = (data.backlinks || []).map((link: any) => ({
urlFrom: link.url_from || '',
urlTo: link.url_to || '',
httpCode: link.http_code_target ?? null,
anchor: link.anchor || '',
domainRatingSource: link.domain_rating_source ?? 0,
}))
return {
success: true,
output: {
brokenBacklinks,
},
}
},
outputs: {
brokenBacklinks: {
type: 'array',
description: 'List of broken backlinks',
items: {
type: 'object',
properties: {
urlFrom: {
type: 'string',
description: 'The URL of the page containing the broken link',
},
urlTo: { type: 'string', description: 'The broken URL being linked to' },
httpCode: {
type: 'number',
description: 'HTTP status code of the broken target URL (e.g., 404, 410)',
optional: true,
},
anchor: { type: 'string', description: 'The anchor text of the link' },
domainRatingSource: {
type: 'number',
description: 'Domain Rating of the linking domain',
},
},
},
},
},
}
+75
View File
@@ -0,0 +1,75 @@
import type { AhrefsDomainRatingParams, AhrefsDomainRatingResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const domainRatingTool: ToolConfig<AhrefsDomainRatingParams, AhrefsDomainRatingResponse> = {
id: 'ahrefs_domain_rating',
name: 'Ahrefs Domain Rating',
description:
"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website's backlink profile on a scale from 0 to 100.",
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain to analyze (e.g., example.com)',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date for historical data in YYYY-MM-DD format (defaults to today)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/domain-rating')
url.searchParams.set('target', params.target)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get domain rating')
}
return {
success: true,
output: {
domainRating: data.domain_rating?.domain_rating ?? 0,
ahrefsRank: data.domain_rating?.ahrefs_rank ?? null,
},
}
},
outputs: {
domainRating: {
type: 'number',
description: 'Domain Rating score (0-100)',
},
ahrefsRank: {
type: 'number',
description: 'Ahrefs Rank - global ranking based on backlink profile strength',
optional: true,
},
},
}
@@ -0,0 +1,100 @@
import type {
AhrefsDomainRatingHistoryParams,
AhrefsDomainRatingHistoryResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const domainRatingHistoryTool: ToolConfig<
AhrefsDomainRatingHistoryParams,
AhrefsDomainRatingHistoryResponse
> = {
id: 'ahrefs_domain_rating_history',
name: 'Ahrefs Domain Rating History',
description:
'Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
dateFrom: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Start date of the historical period, in YYYY-MM-DD format',
},
dateTo: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
},
historyGrouping: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/domain-rating-history')
url.searchParams.set('target', params.target)
url.searchParams.set('date_from', params.dateFrom)
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get domain rating history')
}
const domainRatings = (data.domain_ratings || []).map((item: any) => ({
date: item.date || '',
domainRating: item.domain_rating ?? 0,
}))
return {
success: true,
output: {
domainRatings,
},
}
},
outputs: {
domainRatings: {
type: 'array',
description: 'Historical Domain Rating data points',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'The date of the measurement' },
domainRating: { type: 'number', description: 'Domain Rating score (0-100) on this date' },
},
},
},
},
}
+49
View File
@@ -0,0 +1,49 @@
import { anchorsTool } from '@/tools/ahrefs/anchors'
import { backlinksTool } from '@/tools/ahrefs/backlinks'
import { backlinksStatsTool } from '@/tools/ahrefs/backlinks_stats'
import { batchAnalysisTool } from '@/tools/ahrefs/batch_analysis'
import { brokenBacklinksTool } from '@/tools/ahrefs/broken_backlinks'
import { domainRatingTool } from '@/tools/ahrefs/domain_rating'
import { domainRatingHistoryTool } from '@/tools/ahrefs/domain_rating_history'
import { keywordOverviewTool } from '@/tools/ahrefs/keyword_overview'
import { keywordsHistoryTool } from '@/tools/ahrefs/keywords_history'
import { metricsTool } from '@/tools/ahrefs/metrics'
import { metricsHistoryTool } from '@/tools/ahrefs/metrics_history'
import { organicCompetitorsTool } from '@/tools/ahrefs/organic_competitors'
import { organicKeywordsTool } from '@/tools/ahrefs/organic_keywords'
import { paidPagesTool } from '@/tools/ahrefs/paid_pages'
import { rankTrackerCompetitorsOverviewTool } from '@/tools/ahrefs/rank_tracker_competitors_overview'
import { rankTrackerCompetitorsStatsTool } from '@/tools/ahrefs/rank_tracker_competitors_stats'
import { rankTrackerOverviewTool } from '@/tools/ahrefs/rank_tracker_overview'
import { rankTrackerSerpOverviewTool } from '@/tools/ahrefs/rank_tracker_serp_overview'
import { refdomainsHistoryTool } from '@/tools/ahrefs/refdomains_history'
import { referringDomainsTool } from '@/tools/ahrefs/referring_domains'
import { relatedTermsTool } from '@/tools/ahrefs/related_terms'
import { siteAuditPageExplorerTool } from '@/tools/ahrefs/site_audit_page_explorer'
import { topPagesTool } from '@/tools/ahrefs/top_pages'
export const ahrefsDomainRatingTool = domainRatingTool
export const ahrefsBacklinksTool = backlinksTool
export const ahrefsBacklinksStatsTool = backlinksStatsTool
export const ahrefsReferringDomainsTool = referringDomainsTool
export const ahrefsOrganicKeywordsTool = organicKeywordsTool
export const ahrefsTopPagesTool = topPagesTool
export const ahrefsKeywordOverviewTool = keywordOverviewTool
export const ahrefsBrokenBacklinksTool = brokenBacklinksTool
export const ahrefsMetricsTool = metricsTool
export const ahrefsOrganicCompetitorsTool = organicCompetitorsTool
export const ahrefsRankTrackerOverviewTool = rankTrackerOverviewTool
export const ahrefsRankTrackerSerpOverviewTool = rankTrackerSerpOverviewTool
export const ahrefsRankTrackerCompetitorsOverviewTool = rankTrackerCompetitorsOverviewTool
export const ahrefsRankTrackerCompetitorsStatsTool = rankTrackerCompetitorsStatsTool
export const ahrefsBatchAnalysisTool = batchAnalysisTool
export const ahrefsSiteAuditPageExplorerTool = siteAuditPageExplorerTool
export const ahrefsDomainRatingHistoryTool = domainRatingHistoryTool
export const ahrefsMetricsHistoryTool = metricsHistoryTool
export const ahrefsRefdomainsHistoryTool = refdomainsHistoryTool
export const ahrefsKeywordsHistoryTool = keywordsHistoryTool
export const ahrefsRelatedTermsTool = relatedTermsTool
export const ahrefsAnchorsTool = anchorsTool
export const ahrefsPaidPagesTool = paidPagesTool
export * from '@/tools/ahrefs/types'
+129
View File
@@ -0,0 +1,129 @@
import type {
AhrefsKeywordOverviewParams,
AhrefsKeywordOverviewResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'keyword,volume,difficulty,cpc,clicks,searches_pct_clicks_organic_only,parent_topic,traffic_potential,intents'
export const keywordOverviewTool: ToolConfig<
AhrefsKeywordOverviewParams,
AhrefsKeywordOverviewResponse
> = {
id: 'ahrefs_keyword_overview',
name: 'Ahrefs Keyword Overview',
description:
'Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.',
version: '1.0.0',
params: {
keyword: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The keyword to analyze',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for keyword data. Example: "us", "gb", "de" (default: "us")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/keywords-explorer/overview')
url.searchParams.set('keywords', params.keyword)
url.searchParams.set('country', params.country || 'us')
url.searchParams.set('select', SELECT_FIELDS)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get keyword overview')
}
const result = (data.keywords || [])[0] || {}
return {
success: true,
output: {
overview: {
keyword: result.keyword || '',
searchVolume: result.volume ?? 0,
keywordDifficulty: result.difficulty ?? null,
cpc: typeof result.cpc === 'number' ? result.cpc / 100 : null,
clicks: result.clicks ?? null,
clicksPercentage: result.searches_pct_clicks_organic_only ?? null,
parentTopic: result.parent_topic ?? null,
trafficPotential: result.traffic_potential ?? null,
intents: result.intents ?? null,
},
},
}
},
outputs: {
overview: {
type: 'object',
description: 'Keyword metrics overview',
properties: {
keyword: { type: 'string', description: 'The analyzed keyword' },
searchVolume: { type: 'number', description: 'Monthly search volume' },
keywordDifficulty: {
type: 'number',
description: 'Keyword difficulty score (0-100)',
optional: true,
},
cpc: { type: 'number', description: 'Cost per click in USD', optional: true },
clicks: { type: 'number', description: 'Estimated clicks per month', optional: true },
clicksPercentage: {
type: 'number',
description: 'Percentage of searches that result in an organic click',
optional: true,
},
parentTopic: {
type: 'string',
description: 'The parent topic for this keyword',
optional: true,
},
trafficPotential: {
type: 'number',
description: 'Estimated traffic potential if ranking #1',
optional: true,
},
intents: {
type: 'object',
description:
'Search intent flags (informational, navigational, commercial, transactional, branded, local)',
optional: true,
properties: {
informational: { type: 'boolean', description: 'Query seeks information' },
navigational: { type: 'boolean', description: 'Query seeks a specific site or page' },
commercial: { type: 'boolean', description: 'Query researches a purchase decision' },
transactional: { type: 'boolean', description: 'Query intends to complete a purchase' },
branded: { type: 'boolean', description: 'Query references a specific brand' },
local: { type: 'boolean', description: 'Query seeks local results' },
},
},
},
},
},
}
+126
View File
@@ -0,0 +1,126 @@
import type {
AhrefsKeywordsHistoryParams,
AhrefsKeywordsHistoryResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'date,top3,top4_10,top11_20,top21_50,top51_plus'
export const keywordsHistoryTool: ToolConfig<
AhrefsKeywordsHistoryParams,
AhrefsKeywordsHistoryResponse
> = {
id: 'ahrefs_keywords_history',
name: 'Ahrefs Keywords History',
description:
'Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
dateFrom: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Start date of the historical period, in YYYY-MM-DD format',
},
dateTo: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
},
historyGrouping: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/keywords-history')
url.searchParams.set('target', params.target)
url.searchParams.set('date_from', params.dateFrom)
url.searchParams.set('select', SELECT_FIELDS)
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
url.searchParams.set('country', params.country || 'us')
if (params.mode) url.searchParams.set('mode', params.mode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get keywords history')
}
const keywordsHistory = (data.keywords || []).map((item: any) => ({
date: item.date || '',
top3: item.top3 ?? 0,
top4To10: item.top4_10 ?? 0,
top11To20: item.top11_20 ?? 0,
top21To50: item.top21_50 ?? 0,
top51Plus: item.top51_plus ?? 0,
}))
return {
success: true,
output: {
keywordsHistory,
},
}
},
outputs: {
keywordsHistory: {
type: 'array',
description: 'Historical organic keyword ranking distribution',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date of the record' },
top3: { type: 'number', description: 'Keywords ranking in top 3 organic results' },
top4To10: { type: 'number', description: 'Keywords ranking in positions 4-10' },
top11To20: { type: 'number', description: 'Keywords ranking in positions 11-20' },
top21To50: { type: 'number', description: 'Keywords ranking in positions 21-50' },
top51Plus: { type: 'number', description: 'Keywords ranking in position 51 and beyond' },
},
},
},
},
}
+116
View File
@@ -0,0 +1,116 @@
import type { AhrefsMetricsParams, AhrefsMetricsResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const metricsTool: ToolConfig<AhrefsMetricsParams, AhrefsMetricsResponse> = {
id: 'ahrefs_metrics',
name: 'Ahrefs Metrics',
description:
'Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for traffic data. Example: "us", "gb", "de"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/metrics')
url.searchParams.set('target', params.target)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
url.searchParams.set('country', params.country || 'us')
if (params.mode) url.searchParams.set('mode', params.mode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get metrics')
}
const metrics = data.metrics || {}
return {
success: true,
output: {
metrics: {
organicTraffic: metrics.org_traffic ?? 0,
organicKeywords: metrics.org_keywords ?? 0,
organicKeywordsTop3: metrics.org_keywords_1_3 ?? 0,
organicCost: typeof metrics.org_cost === 'number' ? metrics.org_cost / 100 : null,
paidTraffic: metrics.paid_traffic ?? 0,
paidKeywords: metrics.paid_keywords ?? 0,
paidPages: metrics.paid_pages ?? 0,
paidCost: typeof metrics.paid_cost === 'number' ? metrics.paid_cost / 100 : null,
},
},
}
},
outputs: {
metrics: {
type: 'object',
description: 'Organic and paid search overview',
properties: {
organicTraffic: { type: 'number', description: 'Estimated monthly organic traffic' },
organicKeywords: { type: 'number', description: 'Number of organic keywords ranked' },
organicKeywordsTop3: {
type: 'number',
description: 'Number of organic keywords ranking in positions 1-3',
},
organicCost: {
type: 'number',
description: 'Estimated monthly cost to replicate organic traffic via ads (USD)',
optional: true,
},
paidTraffic: { type: 'number', description: 'Estimated monthly paid search traffic' },
paidKeywords: { type: 'number', description: 'Number of paid keywords targeted' },
paidPages: { type: 'number', description: 'Number of pages receiving paid traffic' },
paidCost: {
type: 'number',
description: 'Estimated monthly paid search spend (USD)',
optional: true,
},
},
},
},
}
+136
View File
@@ -0,0 +1,136 @@
import type { AhrefsMetricsHistoryParams, AhrefsMetricsHistoryResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'date,org_traffic,org_cost,paid_traffic,paid_cost'
export const metricsHistoryTool: ToolConfig<
AhrefsMetricsHistoryParams,
AhrefsMetricsHistoryResponse
> = {
id: 'ahrefs_metrics_history',
name: 'Ahrefs Metrics History',
description:
'Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
dateFrom: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Start date of the historical period, in YYYY-MM-DD format',
},
dateTo: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
},
volumeMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
},
historyGrouping: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/metrics-history')
url.searchParams.set('target', params.target)
url.searchParams.set('date_from', params.dateFrom)
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
url.searchParams.set('select', SELECT_FIELDS)
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
url.searchParams.set('country', params.country || 'us')
if (params.mode) url.searchParams.set('mode', params.mode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get metrics history')
}
const metrics = (data.metrics || []).map((item: any) => ({
date: item.date || '',
organicTraffic: item.org_traffic ?? 0,
organicCost: typeof item.org_cost === 'number' ? item.org_cost / 100 : null,
paidTraffic: item.paid_traffic ?? 0,
paidCost: typeof item.paid_cost === 'number' ? item.paid_cost / 100 : null,
}))
return {
success: true,
output: {
metricsHistory: metrics,
},
}
},
outputs: {
metricsHistory: {
type: 'array',
description: 'Historical organic and paid traffic data points',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date of the metric entry' },
organicTraffic: { type: 'number', description: 'Estimated monthly organic visits' },
organicCost: {
type: 'number',
description: 'Estimated monthly cost to replicate organic traffic via ads (USD)',
optional: true,
},
paidTraffic: { type: 'number', description: 'Estimated monthly paid search visits' },
paidCost: {
type: 'number',
description: 'Estimated monthly paid search spend (USD)',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,138 @@
import type {
AhrefsOrganicCompetitorsParams,
AhrefsOrganicCompetitorsResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'competitor_domain,domain_rating,keywords_common,keywords_target,keywords_competitor,traffic'
export const organicCompetitorsTool: ToolConfig<
AhrefsOrganicCompetitorsParams,
AhrefsOrganicCompetitorsResponse
> = {
id: 'ahrefs_organic_competitors',
name: 'Ahrefs Organic Competitors',
description:
'Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/organic-competitors')
url.searchParams.set('target', params.target)
url.searchParams.set('country', params.country || 'us')
url.searchParams.set('select', SELECT_FIELDS)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get organic competitors')
}
const competitors = (data.competitors || []).map((competitor: any) => ({
domain: competitor.competitor_domain ?? null,
domainRating: competitor.domain_rating ?? 0,
commonKeywords: competitor.keywords_common ?? 0,
targetKeywords: competitor.keywords_target ?? 0,
competitorKeywords: competitor.keywords_competitor ?? 0,
traffic: competitor.traffic ?? null,
}))
return {
success: true,
output: {
competitors,
},
}
},
outputs: {
competitors: {
type: 'array',
description: 'List of organic search competitors ranked by keyword overlap',
items: {
type: 'object',
properties: {
domain: {
type: 'string',
description: 'The competitor domain',
optional: true,
},
domainRating: { type: 'number', description: 'Domain Rating of the competitor' },
commonKeywords: {
type: 'number',
description: 'Number of keywords the competitor and target both rank for',
},
targetKeywords: {
type: 'number',
description: 'Number of keywords the target ranks for',
},
competitorKeywords: {
type: 'number',
description: 'Number of keywords the competitor ranks for',
},
traffic: {
type: 'number',
description: 'Estimated monthly organic traffic for the competitor',
optional: true,
},
},
},
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type {
AhrefsOrganicKeywordsParams,
AhrefsOrganicKeywordsResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'keyword,volume,best_position,best_position_url,sum_traffic,keyword_difficulty'
export const organicKeywordsTool: ToolConfig<
AhrefsOrganicKeywordsParams,
AhrefsOrganicKeywordsResponse
> = {
id: 'ahrefs_organic_keywords',
name: 'Ahrefs Organic Keywords',
description:
'Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for search results. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/organic-keywords')
url.searchParams.set('target', params.target)
url.searchParams.set('country', params.country || 'us')
url.searchParams.set('select', SELECT_FIELDS)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get organic keywords')
}
const keywords = (data.keywords || []).map((kw: any) => ({
keyword: kw.keyword || '',
volume: kw.volume ?? 0,
position: kw.best_position ?? null,
url: kw.best_position_url ?? null,
traffic: kw.sum_traffic ?? 0,
keywordDifficulty: kw.keyword_difficulty ?? null,
}))
return {
success: true,
output: {
keywords,
},
}
},
outputs: {
keywords: {
type: 'array',
description: 'List of organic keywords the target ranks for',
items: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'The keyword' },
volume: { type: 'number', description: 'Monthly search volume' },
position: {
type: 'number',
description: 'Best ranking position for this keyword',
optional: true,
},
url: {
type: 'string',
description: 'The URL that ranks at the best position for this keyword',
optional: true,
},
traffic: { type: 'number', description: 'Estimated monthly organic traffic' },
keywordDifficulty: {
type: 'number',
description: 'Keyword difficulty score (0-100)',
optional: true,
},
},
},
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type { AhrefsPaidPagesParams, AhrefsPaidPagesResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'url,sum_traffic,keywords,top_keyword,value,ads_count'
export const paidPagesTool: ToolConfig<AhrefsPaidPagesParams, AhrefsPaidPagesResponse> = {
id: 'ahrefs_paid_pages',
name: 'Ahrefs Paid Pages',
description:
"Get a target domain's pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.",
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/paid-pages')
url.searchParams.set('target', params.target)
url.searchParams.set('select', SELECT_FIELDS)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
url.searchParams.set('country', params.country || 'us')
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get paid pages')
}
const paidPages = (data.pages || []).map((page: any) => ({
url: page.url ?? null,
traffic: page.sum_traffic ?? null,
keywords: page.keywords ?? null,
topKeyword: page.top_keyword ?? null,
value: typeof page.value === 'number' ? page.value / 100 : null,
adsCount: page.ads_count ?? null,
}))
return {
success: true,
output: {
paidPages,
},
}
},
outputs: {
paidPages: {
type: 'array',
description: 'List of pages receiving paid search traffic',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL', optional: true },
traffic: {
type: 'number',
description: 'Estimated monthly paid search traffic',
optional: true,
},
keywords: {
type: 'number',
description: 'Number of paid keywords the page ranks for',
optional: true,
},
topKeyword: {
type: 'string',
description: 'The top keyword driving paid traffic to this page',
optional: true,
},
value: {
type: 'number',
description: 'Estimated monthly paid traffic cost in USD',
optional: true,
},
adsCount: {
type: 'number',
description: 'Number of unique ads shown for this page',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,167 @@
import type {
AhrefsRankTrackerCompetitorsOverviewParams,
AhrefsRankTrackerCompetitorsOverviewResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'keyword,volume,keyword_difficulty,serp_features,competitors_list'
export const rankTrackerCompetitorsOverviewTool: ToolConfig<
AhrefsRankTrackerCompetitorsOverviewParams,
AhrefsRankTrackerCompetitorsOverviewResponse
> = {
id: 'ahrefs_rank_tracker_competitors_overview',
name: 'Ahrefs Rank Tracker Competitors Overview',
description:
"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword's volume and difficulty alongside every competitor's position, traffic, and traffic value. This endpoint is free and does not consume API units.",
version: '1.0.0',
params: {
projectId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
},
date: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Date to report rankings for, in YYYY-MM-DD format',
},
device: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Rankings device type: "desktop" or "mobile"',
},
dateCompared: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comparison date in YYYY-MM-DD format, to compute position/traffic deltas',
},
volumeMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/competitors-overview')
url.searchParams.set('project_id', String(params.projectId))
url.searchParams.set('date', params.date)
url.searchParams.set('device', params.device)
url.searchParams.set('select', SELECT_FIELDS)
if (params.dateCompared) url.searchParams.set('date_compared', params.dateCompared)
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.error?.message || data.error || 'Failed to get rank tracker competitors overview'
)
}
const competitorKeywords = (data.keywords || []).map((item: any) => ({
keyword: item.keyword || '',
volume: item.volume ?? null,
keywordDifficulty: item.keyword_difficulty ?? null,
serpFeatures: item.serp_features ?? [],
competitorsList: (item.competitors_list || []).map((competitor: any) => ({
url: competitor.url || '',
position: competitor.position ?? null,
bestPositionKind: competitor.best_position_kind ?? null,
traffic: competitor.traffic ?? null,
value: typeof competitor.value === 'number' ? competitor.value / 100 : null,
})),
}))
return {
success: true,
output: {
competitorKeywords,
},
}
},
outputs: {
competitorKeywords: {
type: 'array',
description: 'Tracked keywords with competitor ranking data',
items: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'The tracked keyword' },
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
keywordDifficulty: {
type: 'number',
description: 'Keyword difficulty score (0-100)',
optional: true,
},
serpFeatures: {
type: 'array',
description: 'SERP features present in the results',
items: { type: 'string' },
},
competitorsList: {
type: 'array',
description: 'Ranking data for each tracked competitor on this keyword',
items: {
type: 'object',
properties: {
url: { type: 'string', description: "The competitor's ranking URL" },
position: {
type: 'number',
description: 'Current ranking position',
optional: true,
},
bestPositionKind: {
type: 'string',
description: 'Type of the best position achieved',
optional: true,
},
traffic: {
type: 'number',
description: 'Estimated traffic to the competitor',
optional: true,
},
value: {
type: 'number',
description: 'Estimated traffic value (USD)',
optional: true,
},
},
},
},
},
},
},
},
}
@@ -0,0 +1,132 @@
import type {
AhrefsRankTrackerCompetitorsStatsParams,
AhrefsRankTrackerCompetitorsStatsResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'competitor,traffic,traffic_value,average_position,pos_1_3,pos_4_10,share_of_voice,share_of_traffic_value'
export const rankTrackerCompetitorsStatsTool: ToolConfig<
AhrefsRankTrackerCompetitorsStatsParams,
AhrefsRankTrackerCompetitorsStatsResponse
> = {
id: 'ahrefs_rank_tracker_competitors_stats',
name: 'Ahrefs Rank Tracker Competitors Stats',
description:
"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor's traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.",
version: '1.0.0',
params: {
projectId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
},
date: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Date to report metrics for, in YYYY-MM-DD format',
},
device: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Rankings device type: "desktop" or "mobile"',
},
volumeMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/competitors-stats')
url.searchParams.set('select', SELECT_FIELDS)
url.searchParams.set('date', params.date)
url.searchParams.set('device', params.device)
url.searchParams.set('project_id', String(params.projectId))
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.error?.message || data.error || 'Failed to get rank tracker competitors stats'
)
}
const competitorsStats = (data['competitors-metrics'] || []).map((item: any) => ({
competitor: item.competitor || '',
traffic: item.traffic ?? null,
trafficValue: typeof item.traffic_value === 'number' ? item.traffic_value / 100 : null,
averagePosition: item.average_position ?? null,
pos1To3: item.pos_1_3 ?? 0,
pos4To10: item.pos_4_10 ?? 0,
shareOfVoice: item.share_of_voice ?? 0,
shareOfTrafficValue: item.share_of_traffic_value ?? 0,
}))
return {
success: true,
output: {
competitorsStats,
},
}
},
outputs: {
competitorsStats: {
type: 'array',
description: 'Aggregate stats for each tracked competitor',
items: {
type: 'object',
properties: {
competitor: { type: 'string', description: "The competitor's URL" },
traffic: {
type: 'number',
description: 'Estimated monthly organic visits',
optional: true,
},
trafficValue: {
type: 'number',
description: 'Estimated monthly organic traffic value (USD)',
optional: true,
},
averagePosition: {
type: 'number',
description: 'Average top organic position across tracked keywords',
optional: true,
},
pos1To3: { type: 'number', description: 'Keywords ranking in top 3 positions' },
pos4To10: { type: 'number', description: 'Keywords ranking in positions 4-10' },
shareOfVoice: { type: 'number', description: 'Organic traffic share percentage' },
shareOfTrafficValue: {
type: 'number',
description: 'Organic traffic value share percentage',
},
},
},
},
},
}
@@ -0,0 +1,149 @@
import type {
AhrefsRankTrackerOverviewParams,
AhrefsRankTrackerOverviewResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'keyword,position,volume,keyword_difficulty,url,traffic,serp_features,best_position_kind'
export const rankTrackerOverviewTool: ToolConfig<
AhrefsRankTrackerOverviewParams,
AhrefsRankTrackerOverviewResponse
> = {
id: 'ahrefs_rank_tracker_overview',
name: 'Ahrefs Rank Tracker Overview',
description:
'Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.',
version: '1.0.0',
params: {
projectId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
},
date: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Date to report rankings for, in YYYY-MM-DD format',
},
device: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Rankings device type: "desktop" or "mobile"',
},
dateCompared: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comparison date in YYYY-MM-DD format, to compute position/traffic deltas',
},
volumeMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/overview')
url.searchParams.set('project_id', String(params.projectId))
url.searchParams.set('date', params.date)
url.searchParams.set('device', params.device)
url.searchParams.set('select', SELECT_FIELDS)
if (params.dateCompared) url.searchParams.set('date_compared', params.dateCompared)
url.searchParams.set('volume_mode', params.volumeMode || 'monthly')
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get rank tracker overview')
}
const overviews = (data.overviews || []).map((item: any) => ({
keyword: item.keyword || '',
position: item.position ?? null,
volume: item.volume ?? null,
keywordDifficulty: item.keyword_difficulty ?? null,
url: item.url ?? null,
traffic: item.traffic ?? null,
serpFeatures: item.serp_features ?? [],
bestPositionKind: item.best_position_kind ?? null,
}))
return {
success: true,
output: {
overviews,
},
}
},
outputs: {
overviews: {
type: 'array',
description: 'Ranking overview for each tracked keyword',
items: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'The tracked keyword' },
position: {
type: 'number',
description: 'Top organic search position',
optional: true,
},
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
keywordDifficulty: {
type: 'number',
description: 'Keyword difficulty score (0-100)',
optional: true,
},
url: { type: 'string', description: 'Top-ranking URL', optional: true },
traffic: {
type: 'number',
description: 'Estimated monthly organic visits',
optional: true,
},
serpFeatures: {
type: 'array',
description: 'SERP features present in the results',
items: { type: 'string' },
},
bestPositionKind: {
type: 'string',
description: 'Type of the top position (organic, paid, or SERP feature)',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,166 @@
import type {
AhrefsRankTrackerSerpOverviewParams,
AhrefsRankTrackerSerpOverviewResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const rankTrackerSerpOverviewTool: ToolConfig<
AhrefsRankTrackerSerpOverviewParams,
AhrefsRankTrackerSerpOverviewResponse
> = {
id: 'ahrefs_rank_tracker_serp_overview',
name: 'Ahrefs Rank Tracker SERP Overview',
description:
'Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.',
version: '1.0.0',
params: {
projectId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The Rank Tracker project ID (found in the project URL in Ahrefs)',
},
keyword: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tracked keyword to retrieve SERP data for',
},
country: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Country code for the tracked keyword. Example: "us", "gb", "de"',
},
device: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Rankings device type: "desktop" or "mobile"',
},
topPositions: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of top organic positions to return (defaults to all available)',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format',
},
locationId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Location ID of the tracked keyword, if tracked at a specific location',
},
languageCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Language code of the tracked keyword',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/rank-tracker/serp-overview')
url.searchParams.set('project_id', String(params.projectId))
url.searchParams.set('keyword', params.keyword)
url.searchParams.set('country', params.country)
url.searchParams.set('device', params.device)
if (params.topPositions) url.searchParams.set('top_positions', String(params.topPositions))
if (params.date) url.searchParams.set('date', params.date)
if (params.locationId) url.searchParams.set('location_id', String(params.locationId))
if (params.languageCode) url.searchParams.set('language_code', params.languageCode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get SERP overview')
}
const positions = (data.positions || []).map((item: any) => ({
position: item.position ?? 0,
url: item.url || '',
title: item.title || '',
type: item.type ?? [],
domainRating: item.domain_rating ?? 0,
urlRating: item.url_rating ?? 0,
backlinks: item.backlinks ?? 0,
refdomains: item.refdomains ?? 0,
traffic: item.traffic ?? 0,
value: typeof item.value === 'number' ? item.value / 100 : null,
topKeyword: item.top_keyword ?? null,
topKeywordVolume: item.top_keyword_volume ?? null,
updateDate: item.update_date || '',
}))
return {
success: true,
output: {
positions,
},
}
},
outputs: {
positions: {
type: 'array',
description: 'Every ranking result on the SERP for the tracked keyword',
items: {
type: 'object',
properties: {
position: { type: 'number', description: 'Position of the result in the SERP' },
url: { type: 'string', description: 'URL of the ranking page' },
title: { type: 'string', description: 'Page title' },
type: {
type: 'array',
description: 'The kind of the position: organic, paid, or a SERP feature',
items: { type: 'string' },
},
domainRating: { type: 'number', description: 'Domain Rating of the ranking domain' },
urlRating: { type: 'number', description: 'URL Rating of the ranking page' },
backlinks: { type: 'number', description: 'Total backlinks to the ranking domain' },
refdomains: { type: 'number', description: 'Unique referring domains' },
traffic: { type: 'number', description: 'Estimated monthly organic search traffic' },
value: {
type: 'number',
description: 'Estimated monthly traffic value (USD)',
optional: true,
},
topKeyword: {
type: 'string',
description: 'Highest-traffic keyword ranking for this page',
optional: true,
},
topKeywordVolume: {
type: 'number',
description: 'Monthly search volume for the top keyword',
optional: true,
},
updateDate: { type: 'string', description: 'Date the SERP was last checked' },
},
},
},
},
}
+113
View File
@@ -0,0 +1,113 @@
import type {
AhrefsRefdomainsHistoryParams,
AhrefsRefdomainsHistoryResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
export const refdomainsHistoryTool: ToolConfig<
AhrefsRefdomainsHistoryParams,
AhrefsRefdomainsHistoryResponse
> = {
id: 'ahrefs_refdomains_history',
name: 'Ahrefs Referring Domains History',
description:
'Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain or URL to analyze. Example: "example.com"',
},
dateFrom: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Start date of the historical period, in YYYY-MM-DD format',
},
dateTo: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'End date of the historical period, in YYYY-MM-DD format (defaults to today)',
},
historyGrouping: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time interval for grouping data points: "daily", "weekly", or "monthly" (default: "monthly")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/refdomains-history')
url.searchParams.set('target', params.target)
url.searchParams.set('date_from', params.dateFrom)
if (params.dateTo) url.searchParams.set('date_to', params.dateTo)
if (params.historyGrouping) url.searchParams.set('history_grouping', params.historyGrouping)
if (params.mode) url.searchParams.set('mode', params.mode)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
data.error?.message || data.error || 'Failed to get referring domains history'
)
}
const referringDomainsHistory = (data.refdomains || []).map((item: any) => ({
date: item.date || '',
referringDomains: item.refdomains ?? 0,
}))
return {
success: true,
output: {
referringDomainsHistory,
},
}
},
outputs: {
referringDomainsHistory: {
type: 'array',
description: 'Historical referring domains count data points',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'The date of the data point' },
referringDomains: {
type: 'number',
description: 'Total number of unique domains linking to the target on this date',
},
},
},
},
},
}
+123
View File
@@ -0,0 +1,123 @@
import type {
AhrefsReferringDomainsParams,
AhrefsReferringDomainsResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'domain,domain_rating,links_to_target,dofollow_links,first_seen,last_seen'
export const referringDomainsTool: ToolConfig<
AhrefsReferringDomainsParams,
AhrefsReferringDomainsResponse
> = {
id: 'ahrefs_referring_domains',
name: 'Ahrefs Referring Domains',
description:
'Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
history: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Historical scope: "live" (currently live), "all_time" (default, includes lost domains), or "since:YYYY-MM-DD" (domains found since a date).',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/refdomains')
url.searchParams.set('target', params.target)
url.searchParams.set('select', SELECT_FIELDS)
if (params.mode) url.searchParams.set('mode', params.mode)
url.searchParams.set('history', params.history || 'all_time')
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get referring domains')
}
const referringDomains = (data.refdomains || []).map((domain: any) => ({
domain: domain.domain || '',
domainRating: domain.domain_rating ?? 0,
backlinks: domain.links_to_target ?? 0,
dofollowBacklinks: domain.dofollow_links ?? 0,
firstSeen: domain.first_seen || '',
lastVisited: domain.last_seen ?? null,
}))
return {
success: true,
output: {
referringDomains,
},
}
},
outputs: {
referringDomains: {
type: 'array',
description: 'List of domains linking to the target',
items: {
type: 'object',
properties: {
domain: { type: 'string', description: 'The referring domain' },
domainRating: { type: 'number', description: 'Domain Rating of the referring domain' },
backlinks: {
type: 'number',
description: 'Total number of backlinks from this domain to the target',
},
dofollowBacklinks: {
type: 'number',
description: 'Number of dofollow backlinks from this domain',
},
firstSeen: { type: 'string', description: 'When the domain was first seen linking' },
lastVisited: {
type: 'string',
description: 'When the domain was last seen linking (null if never re-crawled)',
optional: true,
},
},
},
},
},
}
+139
View File
@@ -0,0 +1,139 @@
import type { AhrefsRelatedTermsParams, AhrefsRelatedTermsResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'keyword,volume,difficulty,cpc,parent_topic,traffic_potential,intents,serp_features'
export const relatedTermsTool: ToolConfig<AhrefsRelatedTermsParams, AhrefsRelatedTermsResponse> = {
id: 'ahrefs_related_terms',
name: 'Ahrefs Related Terms',
description:
'Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for ("also rank for") or also discuss ("also talk about"), with volume, difficulty, and CPC.',
version: '1.0.0',
params: {
keyword: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The seed keyword to find related terms for',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for keyword data. Example: "us", "gb", "de" (default: "us")',
},
terms: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Type of related keywords to return: "also_rank_for", "also_talk_about", or "all" (default: "all")',
},
viewFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Whether to derive related terms from the top 10 or top 100 ranking pages (default: "top_10")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/keywords-explorer/related-terms')
url.searchParams.set('select', SELECT_FIELDS)
url.searchParams.set('country', params.country || 'us')
url.searchParams.set('keywords', params.keyword)
if (params.terms) url.searchParams.set('terms', params.terms)
if (params.viewFor) url.searchParams.set('view_for', params.viewFor)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get related terms')
}
const relatedTerms = (data.keywords || []).map((item: any) => ({
keyword: item.keyword || '',
volume: item.volume ?? null,
keywordDifficulty: item.difficulty ?? null,
cpc: typeof item.cpc === 'number' ? item.cpc / 100 : null,
parentTopic: item.parent_topic ?? null,
trafficPotential: item.traffic_potential ?? null,
intents: item.intents ?? null,
serpFeatures: item.serp_features ?? [],
}))
return {
success: true,
output: {
relatedTerms,
},
}
},
outputs: {
relatedTerms: {
type: 'array',
description: 'Related keyword ideas for the seed keyword',
items: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'The related keyword' },
volume: { type: 'number', description: 'Average monthly search volume', optional: true },
keywordDifficulty: {
type: 'number',
description: 'Keyword difficulty score (0-100)',
optional: true,
},
cpc: { type: 'number', description: 'Cost per click in USD', optional: true },
parentTopic: {
type: 'string',
description: 'The parent topic for this keyword',
optional: true,
},
trafficPotential: {
type: 'number',
description: 'Estimated traffic potential if ranking #1',
optional: true,
},
intents: {
type: 'object',
description:
'Search intent flags (informational, navigational, commercial, transactional, branded, local)',
optional: true,
},
serpFeatures: {
type: 'array',
description: 'SERP features present in the results',
items: { type: 'string' },
},
},
},
},
},
}
@@ -0,0 +1,142 @@
import type {
AhrefsSiteAuditPageExplorerParams,
AhrefsSiteAuditPageExplorerResponse,
} from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS =
'url,http_code,title,internal_links,external_links,backlinks,compliant,traffic'
export const siteAuditPageExplorerTool: ToolConfig<
AhrefsSiteAuditPageExplorerParams,
AhrefsSiteAuditPageExplorerResponse
> = {
id: 'ahrefs_site_audit_page_explorer',
name: 'Ahrefs Site Audit Page Explorer',
description:
'Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.',
version: '1.0.0',
params: {
projectId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The Site Audit project ID (found in the project URL in Ahrefs)',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip, for pagination',
},
issueId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return pages affected by this issue ID',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-audit/page-explorer')
url.searchParams.set('project_id', String(params.projectId))
url.searchParams.set('select', SELECT_FIELDS)
if (params.date) url.searchParams.set('date', params.date)
if (params.limit) url.searchParams.set('limit', String(params.limit))
if (params.offset) url.searchParams.set('offset', String(params.offset))
if (params.issueId) url.searchParams.set('issue_id', params.issueId)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get site audit pages')
}
const auditPages = (data.pages || []).map((page: any) => ({
url: page.url || '',
httpCode: page.http_code ?? null,
title: page.title ?? [],
internalLinks: (page.internal_links || []).length,
externalLinks: (page.external_links || []).length,
backlinks: page.backlinks ?? null,
compliant: page.compliant ?? null,
traffic: page.traffic ?? null,
}))
return {
success: true,
output: {
auditPages,
},
}
},
outputs: {
auditPages: {
type: 'array',
description: 'List of crawled pages with health and SEO metrics',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The crawled page URL' },
httpCode: {
type: 'number',
description: 'HTTP status code returned by the URL',
optional: true,
},
title: {
type: 'array',
description: 'Page title tag(s)',
items: { type: 'string' },
},
internalLinks: { type: 'number', description: 'Number of internal outgoing links' },
externalLinks: { type: 'number', description: 'Number of external outgoing links' },
backlinks: {
type: 'number',
description: 'Number of incoming external links to the page',
optional: true,
},
compliant: {
type: 'boolean',
description: 'Whether the page is indexable (200 status, no canonical/noindex)',
optional: true,
},
traffic: {
type: 'number',
description: 'Estimated monthly organic traffic to the page',
optional: true,
},
},
},
},
},
}
+124
View File
@@ -0,0 +1,124 @@
import type { AhrefsTopPagesParams, AhrefsTopPagesResponse } from '@/tools/ahrefs/types'
import type { ToolConfig } from '@/tools/types'
const SELECT_FIELDS = 'url,sum_traffic,keywords,top_keyword,value'
export const topPagesTool: ToolConfig<AhrefsTopPagesParams, AhrefsTopPagesResponse> = {
id: 'ahrefs_top_pages',
name: 'Ahrefs Top Pages',
description:
'Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.',
version: '1.0.0',
params: {
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The target domain to analyze. Example: "example.com"',
},
country: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: "domain"',
},
date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Date to report metrics on, in YYYY-MM-DD format (defaults to today)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ahrefs API Key',
},
},
request: {
url: (params) => {
const url = new URL('https://api.ahrefs.com/v3/site-explorer/top-pages')
url.searchParams.set('target', params.target)
url.searchParams.set('country', params.country || 'us')
url.searchParams.set('select', SELECT_FIELDS)
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
return url.toString()
},
method: 'GET',
headers: (params) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.error || 'Failed to get top pages')
}
const pages = (data.pages || []).map((page: any) => ({
url: page.url ?? null,
traffic: page.sum_traffic ?? 0,
keywords: page.keywords ?? null,
topKeyword: page.top_keyword ?? null,
value: typeof page.value === 'number' ? page.value / 100 : null,
}))
return {
success: true,
output: {
pages,
},
}
},
outputs: {
pages: {
type: 'array',
description: 'List of top pages by organic traffic',
items: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL', optional: true },
traffic: { type: 'number', description: 'Estimated monthly organic traffic' },
keywords: {
type: 'number',
description: 'Number of keywords the page ranks for',
optional: true,
},
topKeyword: {
type: 'string',
description: 'The top keyword driving traffic to this page',
optional: true,
},
value: {
type: 'number',
description: 'Estimated traffic value in USD',
optional: true,
},
},
},
},
},
}
+609
View File
@@ -0,0 +1,609 @@
// Common types for Ahrefs API tools
import type { ToolResponse } from '@/tools/types'
// Common parameters for all Ahrefs tools
interface AhrefsBaseParams {
apiKey: string
}
// Target mode for analysis
export type AhrefsTargetMode = 'domain' | 'prefix' | 'subdomains' | 'exact'
// Historical scope for backlink-profile endpoints (no `date` param on these endpoints)
export type AhrefsHistory = 'live' | 'all_time' | string // `since:YYYY-MM-DD` is also valid
// Domain Rating tool types
export interface AhrefsDomainRatingParams extends AhrefsBaseParams {
target: string
date?: string // Date in YYYY-MM-DD format, defaults to today
}
export interface AhrefsDomainRatingResponse extends ToolResponse {
output: {
domainRating: number
ahrefsRank: number | null
}
}
// Backlinks tool types
export interface AhrefsBacklinksParams extends AhrefsBaseParams {
target: string
mode?: AhrefsTargetMode
history?: AhrefsHistory
limit?: number
}
interface AhrefsBacklink {
urlFrom: string
urlTo: string
anchor: string
domainRatingSource: number
isDofollow: boolean
firstSeen: string
lastVisited: string
}
export interface AhrefsBacklinksResponse extends ToolResponse {
output: {
backlinks: AhrefsBacklink[]
}
}
// Backlinks Stats tool types
export interface AhrefsBacklinksStatsParams extends AhrefsBaseParams {
target: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
}
interface AhrefsBacklinksStatsResult {
liveBacklinks: number
liveReferringDomains: number
allTimeBacklinks: number
allTimeReferringDomains: number
}
export interface AhrefsBacklinksStatsResponse extends ToolResponse {
output: {
stats: AhrefsBacklinksStatsResult
}
}
// Referring Domains tool types
export interface AhrefsReferringDomainsParams extends AhrefsBaseParams {
target: string
mode?: AhrefsTargetMode
history?: AhrefsHistory
limit?: number
}
interface AhrefsReferringDomain {
domain: string
domainRating: number
backlinks: number
dofollowBacklinks: number
firstSeen: string
lastVisited: string | null
}
export interface AhrefsReferringDomainsResponse extends ToolResponse {
output: {
referringDomains: AhrefsReferringDomain[]
}
}
// Organic Keywords tool types
export interface AhrefsOrganicKeywordsParams extends AhrefsBaseParams {
target: string
country?: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
limit?: number
}
interface AhrefsOrganicKeyword {
keyword: string
volume: number
position: number | null
url: string | null
traffic: number
keywordDifficulty: number | null
}
export interface AhrefsOrganicKeywordsResponse extends ToolResponse {
output: {
keywords: AhrefsOrganicKeyword[]
}
}
// Top Pages tool types
export interface AhrefsTopPagesParams extends AhrefsBaseParams {
target: string
country?: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
limit?: number
}
interface AhrefsTopPage {
url: string | null
traffic: number
keywords: number | null
topKeyword: string | null
value: number | null
}
export interface AhrefsTopPagesResponse extends ToolResponse {
output: {
pages: AhrefsTopPage[]
}
}
// Keyword Overview tool types
export interface AhrefsKeywordOverviewParams extends AhrefsBaseParams {
keyword: string
country?: string
}
interface AhrefsKeywordIntents {
informational: boolean
navigational: boolean
commercial: boolean
transactional: boolean
branded: boolean
local: boolean
}
interface AhrefsKeywordOverviewResult {
keyword: string
searchVolume: number
keywordDifficulty: number | null
cpc: number | null
clicks: number | null
clicksPercentage: number | null
parentTopic: string | null
trafficPotential: number | null
intents: AhrefsKeywordIntents | null
}
export interface AhrefsKeywordOverviewResponse extends ToolResponse {
output: {
overview: AhrefsKeywordOverviewResult
}
}
// Broken Backlinks tool types
export interface AhrefsBrokenBacklinksParams extends AhrefsBaseParams {
target: string
mode?: AhrefsTargetMode
limit?: number
}
interface AhrefsBrokenBacklink {
urlFrom: string
urlTo: string
httpCode: number | null
anchor: string
domainRatingSource: number
}
export interface AhrefsBrokenBacklinksResponse extends ToolResponse {
output: {
brokenBacklinks: AhrefsBrokenBacklink[]
}
}
// Metrics tool types (single-call organic + paid search overview)
export interface AhrefsMetricsParams extends AhrefsBaseParams {
target: string
country?: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
}
interface AhrefsMetricsResult {
organicTraffic: number
organicKeywords: number
organicKeywordsTop3: number
organicCost: number | null
paidTraffic: number
paidKeywords: number
paidPages: number
paidCost: number | null
}
export interface AhrefsMetricsResponse extends ToolResponse {
output: {
metrics: AhrefsMetricsResult
}
}
// Organic Competitors tool types
export interface AhrefsOrganicCompetitorsParams extends AhrefsBaseParams {
target: string
country?: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
limit?: number
}
interface AhrefsOrganicCompetitor {
domain: string | null
domainRating: number
commonKeywords: number
targetKeywords: number
competitorKeywords: number
traffic: number | null
}
export interface AhrefsOrganicCompetitorsResponse extends ToolResponse {
output: {
competitors: AhrefsOrganicCompetitor[]
}
}
// Rank Tracker device type
export type AhrefsRankTrackerDevice = 'desktop' | 'mobile'
// Rank Tracker search volume calculation mode
export type AhrefsVolumeMode = 'monthly' | 'average'
// Rank Tracker Overview tool types
export interface AhrefsRankTrackerOverviewParams extends AhrefsBaseParams {
projectId: number
date: string // Date in YYYY-MM-DD format (required by the API)
device: AhrefsRankTrackerDevice
dateCompared?: string
volumeMode?: AhrefsVolumeMode
limit?: number
}
interface AhrefsRankTrackerOverviewItem {
keyword: string
position: number | null
volume: number | null
keywordDifficulty: number | null
url: string | null
traffic: number | null
serpFeatures: string[]
bestPositionKind: string | null
}
export interface AhrefsRankTrackerOverviewResponse extends ToolResponse {
output: {
overviews: AhrefsRankTrackerOverviewItem[]
}
}
// Rank Tracker SERP Overview tool types
export interface AhrefsRankTrackerSerpOverviewParams extends AhrefsBaseParams {
projectId: number
keyword: string
country: string
device: AhrefsRankTrackerDevice
topPositions?: number
date?: string // ISO date-time (YYYY-MM-DDThh:mm:ss)
locationId?: number
languageCode?: string
}
interface AhrefsSerpPosition {
position: number
url: string
title: string
type: string[]
domainRating: number
urlRating: number
backlinks: number
refdomains: number
traffic: number
value: number | null
topKeyword: string | null
topKeywordVolume: number | null
updateDate: string
}
export interface AhrefsRankTrackerSerpOverviewResponse extends ToolResponse {
output: {
positions: AhrefsSerpPosition[]
}
}
// Rank Tracker Competitors Overview tool types
export interface AhrefsRankTrackerCompetitorsOverviewParams extends AhrefsBaseParams {
projectId: number
date: string
device: AhrefsRankTrackerDevice
dateCompared?: string
volumeMode?: AhrefsVolumeMode
limit?: number
}
interface AhrefsCompetitorListItem {
url: string
position: number | null
bestPositionKind: string | null
traffic: number | null
value: number | null
}
interface AhrefsRankTrackerCompetitorsOverviewItem {
keyword: string
volume: number | null
keywordDifficulty: number | null
serpFeatures: string[]
competitorsList: AhrefsCompetitorListItem[]
}
export interface AhrefsRankTrackerCompetitorsOverviewResponse extends ToolResponse {
output: {
competitorKeywords: AhrefsRankTrackerCompetitorsOverviewItem[]
}
}
// Rank Tracker Competitors Stats tool types
export interface AhrefsRankTrackerCompetitorsStatsParams extends AhrefsBaseParams {
projectId: number
date: string
device: AhrefsRankTrackerDevice
volumeMode?: AhrefsVolumeMode
}
interface AhrefsCompetitorStat {
competitor: string
traffic: number | null
trafficValue: number | null
averagePosition: number | null
pos1To3: number
pos4To10: number
shareOfVoice: number
shareOfTrafficValue: number
}
export interface AhrefsRankTrackerCompetitorsStatsResponse extends ToolResponse {
output: {
competitorsStats: AhrefsCompetitorStat[]
}
}
// Batch Analysis tool types
export interface AhrefsBatchAnalysisParams extends AhrefsBaseParams {
targets: string // Comma-separated list of domains/URLs
mode?: AhrefsTargetMode
protocol?: 'both' | 'http' | 'https'
country?: string
volumeMode?: AhrefsVolumeMode
}
interface AhrefsBatchAnalysisResult {
url: string
index: number
domainRating: number | null
ahrefsRank: number | null
backlinks: number | null
referringDomains: number | null
organicTraffic: number | null
organicKeywords: number | null
paidTraffic: number | null
error: string | null
}
export interface AhrefsBatchAnalysisResponse extends ToolResponse {
output: {
results: AhrefsBatchAnalysisResult[]
}
}
// Site Audit Page Explorer tool types
export interface AhrefsSiteAuditPageExplorerParams extends AhrefsBaseParams {
projectId: number
date?: string // ISO date-time (YYYY-MM-DDThh:mm:ss), defaults to most recent crawl
limit?: number
offset?: number
issueId?: string
}
interface AhrefsPageExplorerResult {
url: string
httpCode: number | null
title: string[]
internalLinks: number
externalLinks: number
backlinks: number | null
compliant: boolean | null
traffic: number | null
}
export interface AhrefsSiteAuditPageExplorerResponse extends ToolResponse {
output: {
auditPages: AhrefsPageExplorerResult[]
}
}
// Domain Rating History tool types
export interface AhrefsDomainRatingHistoryParams extends AhrefsBaseParams {
target: string
dateFrom: string
dateTo?: string
historyGrouping?: 'daily' | 'weekly' | 'monthly'
}
interface AhrefsDomainRatingHistoryItem {
date: string
domainRating: number
}
export interface AhrefsDomainRatingHistoryResponse extends ToolResponse {
output: {
domainRatings: AhrefsDomainRatingHistoryItem[]
}
}
// Metrics History tool types
export interface AhrefsMetricsHistoryParams extends AhrefsBaseParams {
target: string
dateFrom: string
dateTo?: string
volumeMode?: AhrefsVolumeMode
historyGrouping?: 'daily' | 'weekly' | 'monthly'
country?: string
mode?: AhrefsTargetMode
}
interface AhrefsMetricsHistoryItem {
date: string
organicTraffic: number
organicCost: number | null
paidTraffic: number
paidCost: number | null
}
export interface AhrefsMetricsHistoryResponse extends ToolResponse {
output: {
metricsHistory: AhrefsMetricsHistoryItem[]
}
}
// Referring Domains History tool types
export interface AhrefsRefdomainsHistoryParams extends AhrefsBaseParams {
target: string
dateFrom: string
dateTo?: string
historyGrouping?: 'daily' | 'weekly' | 'monthly'
mode?: AhrefsTargetMode
}
interface AhrefsRefdomainsHistoryItem {
date: string
referringDomains: number
}
export interface AhrefsRefdomainsHistoryResponse extends ToolResponse {
output: {
referringDomainsHistory: AhrefsRefdomainsHistoryItem[]
}
}
// Keywords History tool types
export interface AhrefsKeywordsHistoryParams extends AhrefsBaseParams {
target: string
dateFrom: string
dateTo?: string
historyGrouping?: 'daily' | 'weekly' | 'monthly'
country?: string
mode?: AhrefsTargetMode
}
interface AhrefsKeywordsHistoryItem {
date: string
top3: number
top4To10: number
top11To20: number
top21To50: number
top51Plus: number
}
export interface AhrefsKeywordsHistoryResponse extends ToolResponse {
output: {
keywordsHistory: AhrefsKeywordsHistoryItem[]
}
}
// Related Terms tool types
export interface AhrefsRelatedTermsParams extends AhrefsBaseParams {
keyword: string
country?: string
terms?: 'also_rank_for' | 'also_talk_about' | 'all'
viewFor?: 'top_10' | 'top_100'
limit?: number
}
interface AhrefsRelatedTerm {
keyword: string
volume: number | null
keywordDifficulty: number | null
cpc: number | null
parentTopic: string | null
trafficPotential: number | null
intents: Record<string, boolean> | null
serpFeatures: string[]
}
export interface AhrefsRelatedTermsResponse extends ToolResponse {
output: {
relatedTerms: AhrefsRelatedTerm[]
}
}
// Anchors tool types
export interface AhrefsAnchorsParams extends AhrefsBaseParams {
target: string
mode?: AhrefsTargetMode
history?: AhrefsHistory
limit?: number
}
interface AhrefsAnchor {
anchor: string
backlinks: number
dofollowBacklinks: number
referringDomains: number
firstSeen: string
lastSeen: string | null
}
export interface AhrefsAnchorsResponse extends ToolResponse {
output: {
anchors: AhrefsAnchor[]
}
}
// Paid Pages tool types
export interface AhrefsPaidPagesParams extends AhrefsBaseParams {
target: string
country?: string
mode?: AhrefsTargetMode
date?: string // Date in YYYY-MM-DD format, defaults to today
limit?: number
}
interface AhrefsPaidPage {
url: string | null
traffic: number | null
keywords: number | null
topKeyword: string | null
value: number | null
adsCount: number | null
}
export interface AhrefsPaidPagesResponse extends ToolResponse {
output: {
paidPages: AhrefsPaidPage[]
}
}
// Union type for all possible responses
export type AhrefsResponse =
| AhrefsDomainRatingResponse
| AhrefsBacklinksResponse
| AhrefsBacklinksStatsResponse
| AhrefsReferringDomainsResponse
| AhrefsOrganicKeywordsResponse
| AhrefsTopPagesResponse
| AhrefsKeywordOverviewResponse
| AhrefsBrokenBacklinksResponse
| AhrefsMetricsResponse
| AhrefsOrganicCompetitorsResponse
| AhrefsRankTrackerOverviewResponse
| AhrefsRankTrackerSerpOverviewResponse
| AhrefsRankTrackerCompetitorsOverviewResponse
| AhrefsRankTrackerCompetitorsStatsResponse
| AhrefsBatchAnalysisResponse
| AhrefsSiteAuditPageExplorerResponse
| AhrefsDomainRatingHistoryResponse
| AhrefsMetricsHistoryResponse
| AhrefsRefdomainsHistoryResponse
| AhrefsKeywordsHistoryResponse
| AhrefsRelatedTermsResponse
| AhrefsAnchorsResponse
| AhrefsPaidPagesResponse
+88
View File
@@ -0,0 +1,88 @@
import type { AirtableCreateParams, AirtableCreateResponse } from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, AirtableCreateResponse> = {
id: 'airtable_create_records',
name: 'Airtable Create Records',
description: 'Write new records to an Airtable table',
version: '1.0.0',
oauth: {
required: true,
provider: 'airtable',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table ID (starts with "tbl") or table name',
},
records: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Array of records to create, each with a `fields` object',
// Example: [{ fields: { "Field 1": "Value1", "Field 2": "Value2" } }]
},
},
request: {
url: (params) =>
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({ records: params.records }),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
records: data.records ?? [],
metadata: {
recordCount: (data.records ?? []).length,
},
},
}
},
outputs: {
records: {
type: 'array',
description: 'Array of created Airtable records',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Record ID' },
createdTime: { type: 'string', description: 'Record creation timestamp' },
fields: { type: 'json', description: 'Record field values' },
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of records created' },
},
},
},
}
+106
View File
@@ -0,0 +1,106 @@
import type { AirtableDeleteParams, AirtableDeleteResponse } from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
export const airtableDeleteRecordsTool: ToolConfig<AirtableDeleteParams, AirtableDeleteResponse> = {
id: 'airtable_delete_records',
name: 'Airtable Delete Records',
description: 'Delete one or more records from an Airtable table by ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'airtable',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table ID (starts with "tbl") or table name',
},
recordIds: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of record IDs to delete (each starts with "rec", e.g., ["recXXXXXXXXXXXXXX"]). Pass a single-element array to delete one record.',
},
},
request: {
url: (params) => {
const base = `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`
const ids = (params.recordIds ?? [])
.map((id) => (id == null ? '' : String(id).trim()))
.filter(Boolean)
if (ids.length === 0) {
throw new Error('At least one record ID is required to delete')
}
if (ids.length > 10) {
throw new Error(
`Airtable deletes at most 10 records per request (received ${ids.length}). Split the delete into batches of 10 or fewer.`
)
}
const queryParams = new URLSearchParams()
for (const id of ids) {
queryParams.append('records[]', id as string)
}
return `${base}?${queryParams.toString()}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
const records = data.records ?? []
return {
success: true,
output: {
records,
metadata: {
recordCount: records.length,
deletedRecordIds: records.map((r: { id: string }) => r.id),
},
},
}
},
outputs: {
records: {
type: 'array',
description: 'Array of deleted Airtable records',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Record ID' },
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of records deleted' },
deletedRecordIds: { type: 'array', description: 'List of deleted record IDs' },
},
},
},
}
+126
View File
@@ -0,0 +1,126 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
export interface AirtableGetBaseSchemaParams {
accessToken: string
baseId: string
}
interface AirtableFieldSchema {
id: string
name: string
type: string
description?: string
options?: Record<string, unknown>
}
interface AirtableViewSchema {
id: string
name: string
type: string
}
interface AirtableTableSchema {
id: string
name: string
description?: string
fields: AirtableFieldSchema[]
views: AirtableViewSchema[]
}
export interface AirtableGetBaseSchemaResponse extends ToolResponse {
output: {
tables: AirtableTableSchema[]
metadata: {
totalTables: number
}
}
}
export const airtableGetBaseSchemaTool: ToolConfig<
AirtableGetBaseSchemaParams,
AirtableGetBaseSchemaResponse
> = {
id: 'airtable_get_base_schema',
name: 'Airtable Get Base Schema',
description: 'Get the schema of all tables, fields, and views in an Airtable base',
version: '1.0.0',
oauth: {
required: true,
provider: 'airtable',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
},
},
request: {
url: (params) => `https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
const tables = (data.tables || []).map((table: Record<string, unknown>) => ({
id: table.id,
name: table.name,
description: table.description,
fields: ((table.fields as Record<string, unknown>[]) || []).map((field) => ({
id: field.id,
name: field.name,
type: field.type,
description: field.description,
options: field.options,
})),
views: ((table.views as Record<string, unknown>[]) || []).map((view) => ({
id: view.id,
name: view.name,
type: view.type,
})),
}))
return {
success: true,
output: {
tables,
metadata: {
totalTables: tables.length,
},
},
}
},
outputs: {
tables: {
type: 'json',
description: 'Array of table schemas with fields and views',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
fields: { type: 'json' },
views: { type: 'json' },
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata including total tables count',
},
},
}

Some files were not shown because too many files have changed in this diff Show More