@atray/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/package.json +40 -0
- package/src/api.js +93 -0
- package/src/index.js +144 -0
- package/src/tools.js +289 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ATRAY MCP
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@atray/mcp)
|
|
4
|
+
|
|
5
|
+
MCP (Model Context Protocol) server for the [ATRAY](https://atray.app) API. Lets an AI
|
|
6
|
+
assistant (Claude, Claude Code, Cursor, and any other MCP client) manage your ATRAY
|
|
7
|
+
campaigns, posts, and brand profile in natural language.
|
|
8
|
+
|
|
9
|
+
> ATRAY is a SaaS that creates Instagram content with AI: you describe your brand, the AI
|
|
10
|
+
> generates posts (image, caption, hashtags), and ATRAY schedules and publishes them for you.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Node.js >= 18
|
|
15
|
+
- An ATRAY account and an **API key**. Generate one in the Studio under
|
|
16
|
+
**Settings → API keys** (`https://studio.atray.app`). The key (`atray_...`) is shown only
|
|
17
|
+
once on creation, so store it safely.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
### Claude Code (CLI), one command
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
claude mcp add atray -e ATRAY_API_KEY=atray_your_key -- npx -y @atray/mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Manual config (Claude Desktop, Cursor, and others)
|
|
28
|
+
|
|
29
|
+
Add this to your client's MCP config (in Claude Desktop, `claude_desktop_config.json`):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"atray": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@atray/mcp"],
|
|
37
|
+
"env": { "ATRAY_API_KEY": "atray_your_key" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Restart the client and the ATRAY tools become available.
|
|
44
|
+
|
|
45
|
+
## Environment variables
|
|
46
|
+
|
|
47
|
+
| Variable | Required | Default | Description |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `ATRAY_API_KEY` | yes | - | Your ATRAY API key (`atray_...`). |
|
|
50
|
+
| `ATRAY_API_URL` | no | `https://api.atray.app` | Override the API base URL (for self-hosted / staging). |
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
| Tool | What it does |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `getBrandProfile` / `updateBrandProfile` | View and update the brand profile. |
|
|
57
|
+
| `listCampaigns` / `createCampaign` / `getCampaign` / `updateCampaign` | Manage campaigns. `createCampaign` generates posts with AI (1 content credit per post). |
|
|
58
|
+
| `listCampaignPosts` | List posts of a campaign. |
|
|
59
|
+
| `listPosts` / `createPost` / `getPost` / `updatePost` | Manage posts (text and carousel). |
|
|
60
|
+
| `regeneratePostText` / `regeneratePostImage` | Regenerate caption or image with AI. |
|
|
61
|
+
| `uploadPostVideo` | Upload a video (mp4/mov/webm, up to 120 MB) as the post media; published to Instagram as a Reel. |
|
|
62
|
+
| `listApiKeys` / `createApiKey` / `updateApiKey` / `revokeApiKey` | Manage your API keys. |
|
|
63
|
+
|
|
64
|
+
Each AI-generated post consumes 1 content credit from your plan. Creating a campaign
|
|
65
|
+
requires a completed brand profile.
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
Full interactive REST reference (Swagger): <https://api.atray.app/docs/>
|
|
70
|
+
|
|
71
|
+
## Local development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/atray-app/mcp.git
|
|
75
|
+
cd mcp
|
|
76
|
+
npm install
|
|
77
|
+
ATRAY_API_KEY=atray_your_key npm start
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The server speaks MCP over stdio.
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- Website: <https://atray.app>
|
|
85
|
+
- Documentation: <https://atray.app/docs.html>
|
|
86
|
+
- Issues: <https://github.com/atray-app/mcp/issues>
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atray/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for ATRAY API - manage campaigns, posts and brand profile via AI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atray-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["mcp", "atray", "ai", "instagram", "content", "social-media", "automation"],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "ATRAY",
|
|
28
|
+
"email": "contato@atray.app",
|
|
29
|
+
"url": "https://atray.app"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://atray.app",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/atray-app/mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/atray-app/mcp/issues",
|
|
38
|
+
"email": "contato@atray.app"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client for the ATRAY API.
|
|
3
|
+
* All requests are authenticated via the ATRAY_API_KEY environment variable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const BASE_URL = (process.env.ATRAY_API_URL || 'https://api.atray.app').replace(/\/$/, '');
|
|
7
|
+
const API_KEY = process.env.ATRAY_API_KEY || '';
|
|
8
|
+
|
|
9
|
+
if (!API_KEY) {
|
|
10
|
+
process.stderr.write('[atray-mcp] WARNING: ATRAY_API_KEY not set\n');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} method - HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
15
|
+
* @param {string} path - API path, e.g. '/campaigns'
|
|
16
|
+
* @param {object} [body] - Request body (JSON)
|
|
17
|
+
* @param {object} [query] - Query string params
|
|
18
|
+
*/
|
|
19
|
+
async function request(method, path, { body, query } = {}) {
|
|
20
|
+
let url = BASE_URL + path;
|
|
21
|
+
|
|
22
|
+
if (query) {
|
|
23
|
+
const params = Object.entries(query)
|
|
24
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
25
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
26
|
+
if (params.length) url += '?' + params.join('&');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const headers = {
|
|
30
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Accept': 'application/json',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const res = await fetch(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers,
|
|
38
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return parseResponse(res);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Multipart/form-data upload (e.g. post video). The Content-Type header
|
|
46
|
+
* (with boundary) is set automatically by fetch from the FormData body.
|
|
47
|
+
* @param {string} path - API path, e.g. '/posts/<id>/video'
|
|
48
|
+
* @param {object} opts
|
|
49
|
+
* @param {string} opts.field - Form field name, e.g. 'video'
|
|
50
|
+
* @param {Buffer} opts.buffer - File contents
|
|
51
|
+
* @param {string} opts.filename - Original file name (extension matters)
|
|
52
|
+
* @param {string} opts.contentType - File MIME type
|
|
53
|
+
*/
|
|
54
|
+
async function upload(path, { field, buffer, filename, contentType }) {
|
|
55
|
+
const form = new FormData();
|
|
56
|
+
form.append(field, new Blob([buffer], { type: contentType }), filename);
|
|
57
|
+
|
|
58
|
+
const res = await fetch(BASE_URL + path, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
62
|
+
'Accept': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: form,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return parseResponse(res);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function parseResponse(res) {
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
let data;
|
|
73
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const msg = data?.error || data?.message || `HTTP ${res.status}`;
|
|
77
|
+
const err = new Error(`ATRAY API error ${res.status}: ${msg}`);
|
|
78
|
+
err.status = res.status;
|
|
79
|
+
err.data = data;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const api = {
|
|
87
|
+
get: (path, query) => request('GET', path, { query }),
|
|
88
|
+
post: (path, body, query) => request('POST', path, { body, query }),
|
|
89
|
+
put: (path, body) => request('PUT', path, { body }),
|
|
90
|
+
patch: (path, body) => request('PATCH', path, { body }),
|
|
91
|
+
delete: (path) => request('DELETE', path),
|
|
92
|
+
upload,
|
|
93
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { tools } from './tools.js';
|
|
10
|
+
import { api } from './api.js';
|
|
11
|
+
|
|
12
|
+
const server = new Server(
|
|
13
|
+
{ name: 'atray-mcp', version: '1.0.0' },
|
|
14
|
+
{ capabilities: { tools: {} } }
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20
|
+
const { name, arguments: args = {} } = request.params;
|
|
21
|
+
try {
|
|
22
|
+
const result = await callTool(name, args);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function callTool(name, a) {
|
|
30
|
+
switch (name) {
|
|
31
|
+
// ─── BRAND ──────────────────────────────────────────────────────────────
|
|
32
|
+
case 'getBrandProfile':
|
|
33
|
+
return api.get('/brand/profile');
|
|
34
|
+
|
|
35
|
+
case 'updateBrandProfile':
|
|
36
|
+
return api.put('/brand/profile', a);
|
|
37
|
+
|
|
38
|
+
// ─── CAMPAIGNS ──────────────────────────────────────────────────────────
|
|
39
|
+
case 'listCampaigns':
|
|
40
|
+
return api.get('/campaigns', a);
|
|
41
|
+
|
|
42
|
+
case 'createCampaign':
|
|
43
|
+
return api.post('/campaigns', a);
|
|
44
|
+
|
|
45
|
+
case 'getCampaign': {
|
|
46
|
+
const { id, ...q } = a;
|
|
47
|
+
return api.get(`/campaigns/${id}`, q);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'updateCampaign': {
|
|
51
|
+
const { id, ...body } = a;
|
|
52
|
+
return api.patch(`/campaigns/${id}`, body);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'listCampaignPosts': {
|
|
56
|
+
const { id, ...q } = a;
|
|
57
|
+
return api.get(`/campaigns/${id}/posts`, q);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── POSTS ──────────────────────────────────────────────────────────────
|
|
61
|
+
case 'listPosts':
|
|
62
|
+
return api.get('/posts', a);
|
|
63
|
+
|
|
64
|
+
case 'createPost':
|
|
65
|
+
return api.post('/posts', a);
|
|
66
|
+
|
|
67
|
+
case 'getPost':
|
|
68
|
+
return api.get(`/posts/${a.id}`);
|
|
69
|
+
|
|
70
|
+
case 'updatePost': {
|
|
71
|
+
const { id, ...body } = a;
|
|
72
|
+
return api.patch(`/posts/${id}`, body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'regeneratePostText':
|
|
76
|
+
return api.post(`/posts/${a.id}/regenerate-text`);
|
|
77
|
+
|
|
78
|
+
case 'regeneratePostImage': {
|
|
79
|
+
const { id, ...body } = a;
|
|
80
|
+
return api.post(`/posts/${id}/regenerate-image`, body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'uploadPostVideo':
|
|
84
|
+
return uploadPostVideo(a);
|
|
85
|
+
|
|
86
|
+
// ─── API KEYS ────────────────────────────────────────────────────────────
|
|
87
|
+
case 'listApiKeys':
|
|
88
|
+
return api.get('/api-keys');
|
|
89
|
+
|
|
90
|
+
case 'createApiKey':
|
|
91
|
+
return api.post('/api-keys', a);
|
|
92
|
+
|
|
93
|
+
case 'updateApiKey': {
|
|
94
|
+
const { id, ...body } = a;
|
|
95
|
+
return api.patch(`/api-keys/${id}`, body);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'revokeApiKey':
|
|
99
|
+
return api.delete(`/api-keys/${a.id}`);
|
|
100
|
+
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const VIDEO_MIME = { mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm' };
|
|
107
|
+
const VIDEO_MAX_BYTES = 120 * 1024 * 1024;
|
|
108
|
+
|
|
109
|
+
/** Sobe um vídeo (arquivo local ou URL) como mídia do post. Publicado no Instagram vira Reel. */
|
|
110
|
+
async function uploadPostVideo({ id, file_path, video_url }) {
|
|
111
|
+
if (!id) throw new Error('id (post UUID) is required');
|
|
112
|
+
if (!file_path && !video_url) throw new Error('Provide file_path (local file) or video_url (public URL)');
|
|
113
|
+
|
|
114
|
+
let buffer;
|
|
115
|
+
let filename;
|
|
116
|
+
if (file_path) {
|
|
117
|
+
buffer = await readFile(file_path);
|
|
118
|
+
filename = basename(file_path);
|
|
119
|
+
} else {
|
|
120
|
+
const res = await fetch(video_url);
|
|
121
|
+
if (!res.ok) throw new Error(`Failed to download video_url: HTTP ${res.status}`);
|
|
122
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
123
|
+
filename = basename(new URL(video_url).pathname) || 'video.mp4';
|
|
124
|
+
if (!/\.(mp4|mov|webm)$/i.test(filename)) {
|
|
125
|
+
const ct = (res.headers.get('content-type') || '').toLowerCase();
|
|
126
|
+
const ext = Object.keys(VIDEO_MIME).find((k) => VIDEO_MIME[k] === ct.split(';')[0].trim());
|
|
127
|
+
if (ext) filename = 'video.' + ext;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const m = filename.toLowerCase().match(/\.(mp4|mov|webm)$/);
|
|
132
|
+
if (!m) throw new Error('Video must be .mp4, .mov or .webm');
|
|
133
|
+
if (buffer.length > VIDEO_MAX_BYTES) throw new Error('Video exceeds the 120 MB limit');
|
|
134
|
+
|
|
135
|
+
return api.upload(`/posts/${id}/video`, {
|
|
136
|
+
field: 'video',
|
|
137
|
+
buffer,
|
|
138
|
+
filename,
|
|
139
|
+
contentType: VIDEO_MIME[m[1]],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const transport = new StdioServerTransport();
|
|
144
|
+
await server.connect(transport);
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool definitions for the ATRAY API.
|
|
3
|
+
* Each tool maps directly to an x-mcp-enabled endpoint in openapi.yaml.
|
|
4
|
+
*
|
|
5
|
+
* Schema convention:
|
|
6
|
+
* inputSchema follows JSON Schema (Draft 7) - the MCP SDK validates inputs before calling handler.
|
|
7
|
+
* All IDs are UUID strings. All dates are ISO-8601.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const tools = [
|
|
11
|
+
// ─── BRAND ────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
name: 'getBrandProfile',
|
|
15
|
+
description: "Returns the authenticated user's brand profile: name, description, target audience, tone of voice and CTA default.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
name: 'updateBrandProfile',
|
|
24
|
+
description: "Updates the brand profile. Only send the fields you want to change.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
name: { type: 'string', description: 'Brand/company name' },
|
|
29
|
+
description: { type: 'string', description: 'Business description' },
|
|
30
|
+
target_audience: { type: 'string', description: 'Target audience' },
|
|
31
|
+
tone: { type: 'string', enum: ['professional', 'friendly', 'direct', 'premium'], description: 'Tone of voice' },
|
|
32
|
+
cta_default: { type: 'string', description: 'Default call-to-action text for posts' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// ─── CAMPAIGNS ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
name: 'listCampaigns',
|
|
41
|
+
description: 'Lists the user\'s campaigns with optional filters. Returns items array and total count.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
status: { type: 'string', enum: ['active', 'generating', 'archived'], description: 'Filter by status' },
|
|
46
|
+
q: { type: 'string', description: 'Search by name or theme' },
|
|
47
|
+
sort: { type: 'string', enum: ['name', 'theme', 'created_at', 'status', 'posts_count'], description: 'Sort field (default: created_at)' },
|
|
48
|
+
order: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction (default: desc)' },
|
|
49
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Results per page (default: 20)' },
|
|
50
|
+
offset: { type: 'integer', minimum: 0, description: 'Pagination offset (default: 0)' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
name: 'createCampaign',
|
|
57
|
+
description: 'Creates a new campaign and automatically generates posts via AI (1 credit per post). Requires a completed brand profile. Returns the created campaign.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
required: ['name'],
|
|
61
|
+
properties: {
|
|
62
|
+
name: { type: 'string', maxLength: 255, description: 'Campaign name (required)' },
|
|
63
|
+
objective: { type: 'string', enum: ['awareness', 'engagement', 'sales', 'traffic', 'leads'], description: 'Campaign objective' },
|
|
64
|
+
theme: { type: 'string', description: 'Central theme or subject of the campaign' },
|
|
65
|
+
context_text: { type: 'string', description: 'Context or brief for AI content generation (min 20 chars)' },
|
|
66
|
+
start_date: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
67
|
+
end_date: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
68
|
+
frequency: { type: 'integer', description: 'Posts per week' },
|
|
69
|
+
post_time: { type: 'string', description: 'Preferred publish time (HH:MM)' },
|
|
70
|
+
cta_text: { type: 'string', description: 'Call-to-action text' },
|
|
71
|
+
cta_link: { type: 'string', description: 'Call-to-action URL' },
|
|
72
|
+
hashtags: { type: 'array', items: { type: 'string' }, description: 'Hashtags (without #)' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
name: 'getCampaign',
|
|
79
|
+
description: 'Returns a single campaign by ID.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: ['id'],
|
|
83
|
+
properties: {
|
|
84
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
name: 'updateCampaign',
|
|
91
|
+
description: 'Updates a campaign. Only send the fields you want to change.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
required: ['id'],
|
|
95
|
+
properties: {
|
|
96
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
97
|
+
name: { type: 'string', maxLength: 255 },
|
|
98
|
+
status: { type: 'string', enum: ['active', 'generating', 'archived'] },
|
|
99
|
+
objective: { type: 'string', enum: ['awareness', 'engagement', 'sales', 'traffic', 'leads'] },
|
|
100
|
+
theme: { type: 'string' },
|
|
101
|
+
context_text: { type: 'string', description: 'Min 20 chars' },
|
|
102
|
+
start_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
103
|
+
end_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
104
|
+
frequency: { type: 'integer' },
|
|
105
|
+
post_time: { type: 'string', description: 'HH:MM' },
|
|
106
|
+
cta_text: { type: 'string' },
|
|
107
|
+
cta_link: { type: 'string' },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
name: 'listCampaignPosts',
|
|
114
|
+
description: 'Lists all posts belonging to a specific campaign with optional filters.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
required: ['id'],
|
|
118
|
+
properties: {
|
|
119
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
120
|
+
status: { type: 'string', enum: ['draft', 'scheduled', 'published'], description: 'Filter by post status' },
|
|
121
|
+
sort: { type: 'string', enum: ['scheduled_at', 'created_at', 'status'], description: 'Sort field (default: scheduled_at)' },
|
|
122
|
+
order: { type: 'string', enum: ['asc', 'desc'] },
|
|
123
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Default: 20' },
|
|
124
|
+
offset: { type: 'integer', minimum: 0, description: 'Default: 0' },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ─── POSTS ────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
name: 'listPosts',
|
|
133
|
+
description: 'Lists posts with optional filters. Use campaign_id to filter by campaign, or the string "null" to list standalone posts (not linked to any campaign). Without campaign_id returns all posts.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
campaign_id: { type: 'string', description: 'Campaign UUID to filter, or the literal string "null" for standalone posts' },
|
|
138
|
+
status: { type: 'string', enum: ['draft', 'scheduled', 'published'] },
|
|
139
|
+
sort: { type: 'string', enum: ['scheduled_at', 'created_at', 'status'], description: 'Default: scheduled_at' },
|
|
140
|
+
order: { type: 'string', enum: ['asc', 'desc'] },
|
|
141
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Default: 20' },
|
|
142
|
+
offset: { type: 'integer', minimum: 0, description: 'Default: 0' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
name: 'createPost',
|
|
149
|
+
description: 'Creates a standalone post draft. Use type="carousel" with image_descriptions[] (3-6 items) for a carousel post. Default type="text" uses image_description for a single image.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
campaign_id: { type: 'string', description: 'Campaign UUID to link to (optional)' },
|
|
154
|
+
type: { type: 'string', enum: ['text', 'carousel'], description: 'Post type (default: text)' },
|
|
155
|
+
caption_text: { type: 'string', description: 'Post caption text' },
|
|
156
|
+
cta: { type: 'string', description: 'Call-to-action text (e.g. "Link in bio")' },
|
|
157
|
+
hashtags: { type: 'array', items: { type: 'string' }, description: 'Hashtags (without #)' },
|
|
158
|
+
image_description: { type: 'string', description: 'Image description for single-image posts (type=text)' },
|
|
159
|
+
image_descriptions: { type: 'array', items: { type: 'string' }, description: 'Image description per slide for carousel posts (type=carousel, 3-6 items)' },
|
|
160
|
+
context: { type: 'string', description: 'Context/brief for AI text generation (required for standalone posts)' },
|
|
161
|
+
image_text_enabled: { type: 'boolean', description: 'Include text overlay in generated image (default: true)' },
|
|
162
|
+
image_logo_enabled: { type: 'boolean', description: 'Include brand logo in generated image (default: true)' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
name: 'getPost',
|
|
169
|
+
description: 'Returns a single post by ID, including media history and scheduling info.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
required: ['id'],
|
|
173
|
+
properties: {
|
|
174
|
+
id: { type: 'string', description: 'Post UUID' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
name: 'updatePost',
|
|
181
|
+
description: 'Updates a post. Only send the fields you want to change. Supports updating caption, CTA, hashtags, image description, context and image generation settings.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
required: ['id'],
|
|
185
|
+
properties: {
|
|
186
|
+
id: { type: 'string', description: 'Post UUID' },
|
|
187
|
+
caption_text: { type: 'string' },
|
|
188
|
+
cta: { type: 'string' },
|
|
189
|
+
hashtags: { type: 'array', items: { type: 'string' } },
|
|
190
|
+
image_description: { type: 'string', description: 'Image description for single-image posts' },
|
|
191
|
+
image_descriptions: { type: 'array', items: { type: 'string' }, description: 'Image descriptions per slide for carousel posts (3-6 items)' },
|
|
192
|
+
context: { type: 'string', description: 'Context/brief for AI text generation' },
|
|
193
|
+
status: { type: 'string', enum: ['draft', 'scheduled', 'published'] },
|
|
194
|
+
scheduled_at: { type: 'string', description: 'Publish datetime (ISO-8601, must be in the future)' },
|
|
195
|
+
image_text_enabled: { type: 'boolean' },
|
|
196
|
+
image_logo_enabled: { type: 'boolean' },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
name: 'regeneratePostText',
|
|
203
|
+
description: 'Regenerates the post caption and hashtags using AI. For standalone posts (no campaign), context must be filled in the post.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
required: ['id'],
|
|
207
|
+
properties: {
|
|
208
|
+
id: { type: 'string', description: 'Post UUID' },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
name: 'regeneratePostImage',
|
|
215
|
+
description: 'Generates a new image for the post using AI. For carousel posts, regenerates all slides or a specific slide via slot_index. From the 6th generation onwards, an extra credit is charged - confirm with confirm_extra_content: true.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
required: ['id'],
|
|
219
|
+
properties: {
|
|
220
|
+
id: { type: 'string', description: 'Post UUID' },
|
|
221
|
+
confirm_extra_content: { type: 'boolean', description: 'Set true to confirm extra credit charge (6th+ generation)' },
|
|
222
|
+
slot_index: { type: 'integer', minimum: 0, maximum: 5, description: 'Carousel slide index to regenerate (omit to regenerate all slides)' },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
{
|
|
228
|
+
name: 'uploadPostVideo',
|
|
229
|
+
description: 'Uploads a video (mp4, mov or webm; up to 120 MB) as the post media, replacing the current image/video. Provide either file_path (local file) or video_url (public URL to download from). When the post is published to Instagram, videos are posted as Reels (also shared to the feed). Scheduling works the same as image posts.',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
required: ['id'],
|
|
233
|
+
properties: {
|
|
234
|
+
id: { type: 'string', description: 'Post UUID' },
|
|
235
|
+
file_path: { type: 'string', description: 'Absolute path to a local video file (mp4, mov, webm)' },
|
|
236
|
+
video_url: { type: 'string', description: 'Public URL of the video to download and upload' },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// ─── API KEYS ─────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
name: 'listApiKeys',
|
|
245
|
+
description: "Lists the user's active (non-revoked) API keys. The full key is never returned after creation.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
name: 'createApiKey',
|
|
254
|
+
description: 'Creates a new API key. The full key (atray_...) is returned ONLY in this response - save it immediately. Limit: 10 keys per user.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
required: ['name'],
|
|
258
|
+
properties: {
|
|
259
|
+
name: { type: 'string', maxLength: 100, description: 'Descriptive name for the key' },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
{
|
|
265
|
+
name: 'updateApiKey',
|
|
266
|
+
description: 'Updates the name and/or active status of an API key. Inactive keys are rejected at authentication.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
required: ['id'],
|
|
270
|
+
properties: {
|
|
271
|
+
id: { type: 'string', description: 'API key UUID' },
|
|
272
|
+
name: { type: 'string', maxLength: 100, description: 'New name for the key' },
|
|
273
|
+
is_active: { type: 'boolean', description: 'Activate or deactivate the key' },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
name: 'revokeApiKey',
|
|
280
|
+
description: 'Permanently revokes an API key. This action cannot be undone.',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
required: ['id'],
|
|
284
|
+
properties: {
|
|
285
|
+
id: { type: 'string', description: 'API key UUID' },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
];
|