directify-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/.gitattributes +2 -0
- package/README.md +172 -0
- package/package.json +31 -0
- package/src/api.js +77 -0
- package/src/index.js +56 -0
- package/src/tools.js +624 -0
package/.gitattributes
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Directify MCP Server
|
|
2
|
+
|
|
3
|
+
[MCP (Model Context Protocol)](https://modelcontextprotocol.io) server for [Directify](https://directify.app) - manage your directory websites through AI assistants like Claude, Cursor, and other MCP-compatible tools.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
This MCP server lets AI assistants directly manage your Directify directories. Ask Claude to create listings, update categories, publish articles, and more - all through natural language.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g directify-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### Claude Desktop
|
|
18
|
+
|
|
19
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"directify": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "directify-mcp"],
|
|
27
|
+
"env": {
|
|
28
|
+
"DIRECTIFY_API_TOKEN": "your-api-token-here",
|
|
29
|
+
"DIRECTIFY_DIRECTORY_ID": "123"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Claude Code
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
claude mcp add directify -- npx -y directify-mcp
|
|
40
|
+
|
|
41
|
+
# Then set environment variables in your shell:
|
|
42
|
+
export DIRECTIFY_API_TOKEN="your-api-token-here"
|
|
43
|
+
export DIRECTIFY_DIRECTORY_ID="123"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Cursor
|
|
47
|
+
|
|
48
|
+
Add to `.cursor/mcp.json` in your project:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"directify": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "directify-mcp"],
|
|
56
|
+
"env": {
|
|
57
|
+
"DIRECTIFY_API_TOKEN": "your-api-token-here",
|
|
58
|
+
"DIRECTIFY_DIRECTORY_ID": "123"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
| Environment Variable | Required | Description |
|
|
68
|
+
|---------------------|----------|-------------|
|
|
69
|
+
| `DIRECTIFY_API_TOKEN` | Yes | Your API token from Settings > API |
|
|
70
|
+
| `DIRECTIFY_DIRECTORY_ID` | No | Default directory ID (can also pass per-tool) |
|
|
71
|
+
|
|
72
|
+
### Getting your API token
|
|
73
|
+
|
|
74
|
+
1. Go to your [Directify dashboard](https://directify.app)
|
|
75
|
+
2. Navigate to **Settings > API**
|
|
76
|
+
3. Click the **Key icon** to generate a new token
|
|
77
|
+
4. Copy the token
|
|
78
|
+
|
|
79
|
+
### Finding your Directory ID
|
|
80
|
+
|
|
81
|
+
Ask Claude: *"List my Directify directories"* - or find it in the URL when viewing your directory in the dashboard (`/app/{directory_id}/...`).
|
|
82
|
+
|
|
83
|
+
## Available Tools
|
|
84
|
+
|
|
85
|
+
### Directories
|
|
86
|
+
|
|
87
|
+
| Tool | Description |
|
|
88
|
+
|------|-------------|
|
|
89
|
+
| `list_directories` | List all directories you own |
|
|
90
|
+
|
|
91
|
+
### Categories
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `list_categories` | List all categories in a directory |
|
|
96
|
+
| `get_category` | Get details of a specific category |
|
|
97
|
+
| `create_category` | Create a new category |
|
|
98
|
+
| `update_category` | Update a category |
|
|
99
|
+
| `delete_category` | Delete a category |
|
|
100
|
+
|
|
101
|
+
### Tags
|
|
102
|
+
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `list_tags` | List all tags |
|
|
106
|
+
| `get_tag` | Get a specific tag |
|
|
107
|
+
| `create_tag` | Create a new tag |
|
|
108
|
+
| `update_tag` | Update a tag |
|
|
109
|
+
| `delete_tag` | Delete a tag |
|
|
110
|
+
|
|
111
|
+
### Custom Fields
|
|
112
|
+
|
|
113
|
+
| Tool | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `list_custom_fields` | List all custom fields (useful to know field names for listings) |
|
|
116
|
+
|
|
117
|
+
### Listings
|
|
118
|
+
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
|------|-------------|
|
|
121
|
+
| `list_listings` | List all listings (paginated) |
|
|
122
|
+
| `get_listing` | Get full listing details with custom fields |
|
|
123
|
+
| `create_listing` | Create a new listing with custom field values |
|
|
124
|
+
| `update_listing` | Update a listing |
|
|
125
|
+
| `delete_listing` | Delete a listing |
|
|
126
|
+
| `check_listing_exists` | Check if a URL already exists |
|
|
127
|
+
| `bulk_create_listings` | Create up to 100 listings at once |
|
|
128
|
+
|
|
129
|
+
### Articles
|
|
130
|
+
|
|
131
|
+
| Tool | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `list_articles` | List all blog articles (paginated) |
|
|
134
|
+
| `get_article` | Get article details |
|
|
135
|
+
| `create_article` | Create a new article (HTML or Markdown) |
|
|
136
|
+
| `update_article` | Update an article |
|
|
137
|
+
| `delete_article` | Delete an article |
|
|
138
|
+
| `toggle_article` | Toggle active/inactive status |
|
|
139
|
+
|
|
140
|
+
## Example Conversations
|
|
141
|
+
|
|
142
|
+
### Create a listing
|
|
143
|
+
|
|
144
|
+
> **You:** Add a new restaurant called "Bella Trattoria" at 123 Main St, Italian cuisine, price range $$$, open Mon-Sat 11am-10pm.
|
|
145
|
+
|
|
146
|
+
Claude will use `create_listing` with the appropriate fields.
|
|
147
|
+
|
|
148
|
+
### Bulk import
|
|
149
|
+
|
|
150
|
+
> **You:** Create 5 test listings for Japanese restaurants in San Francisco with different names and addresses.
|
|
151
|
+
|
|
152
|
+
Claude will use `bulk_create_listings` to create all 5 at once.
|
|
153
|
+
|
|
154
|
+
### Manage content
|
|
155
|
+
|
|
156
|
+
> **You:** Write a blog article about the top 10 Italian restaurants and publish it.
|
|
157
|
+
|
|
158
|
+
Claude will use `create_article` with markdown content.
|
|
159
|
+
|
|
160
|
+
### Update listings
|
|
161
|
+
|
|
162
|
+
> **You:** Update all listings that don't have a description and add a short one based on their name and category.
|
|
163
|
+
|
|
164
|
+
Claude will use `list_listings`, then `update_listing` for each one.
|
|
165
|
+
|
|
166
|
+
## Rate Limits
|
|
167
|
+
|
|
168
|
+
The Directify API allows **120 requests per minute** per directory. The MCP server handles rate limit errors gracefully and will inform the AI assistant to retry.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "directify-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Directify - manage your directory websites through AI assistants like Claude.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"directify-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"directify",
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"claude",
|
|
18
|
+
"ai",
|
|
19
|
+
"directory",
|
|
20
|
+
"listings",
|
|
21
|
+
"api"
|
|
22
|
+
],
|
|
23
|
+
"author": "Directify",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const BASE_URL = 'https://directify.app/api';
|
|
2
|
+
|
|
3
|
+
export function getConfig() {
|
|
4
|
+
const token = process.env.DIRECTIFY_API_TOKEN;
|
|
5
|
+
const directoryId = process.env.DIRECTIFY_DIRECTORY_ID;
|
|
6
|
+
|
|
7
|
+
if (!token) {
|
|
8
|
+
throw new Error('DIRECTIFY_API_TOKEN environment variable is required');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return { token, directoryId };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveDirectory(directoryId) {
|
|
15
|
+
const dir = directoryId || getConfig().directoryId;
|
|
16
|
+
if (!dir) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'No directory ID provided. Pass directory_id parameter or set DIRECTIFY_DIRECTORY_ID environment variable.'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function request(method, path, body = null) {
|
|
25
|
+
const { token } = getConfig();
|
|
26
|
+
|
|
27
|
+
const url = `${BASE_URL}${path}`;
|
|
28
|
+
const headers = {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
Accept: 'application/json',
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const options = { method, headers };
|
|
35
|
+
if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
36
|
+
options.body = JSON.stringify(body);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res = await fetch(url, options);
|
|
40
|
+
|
|
41
|
+
if (res.status === 401) {
|
|
42
|
+
throw new Error('Authentication failed. Check your DIRECTIFY_API_TOKEN.');
|
|
43
|
+
}
|
|
44
|
+
if (res.status === 403) {
|
|
45
|
+
throw new Error('Access denied. You do not have permission to access this resource.');
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 404) {
|
|
48
|
+
throw new Error('Resource not found.');
|
|
49
|
+
}
|
|
50
|
+
if (res.status === 422) {
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
const errors = data.errors
|
|
53
|
+
? Object.entries(data.errors)
|
|
54
|
+
.map(([field, msgs]) => `${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`)
|
|
55
|
+
.join('\n')
|
|
56
|
+
: data.message || 'Validation failed';
|
|
57
|
+
throw new Error(`Validation error:\n${errors}`);
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 429) {
|
|
60
|
+
throw new Error('Rate limit exceeded (120 requests/minute). Please wait and retry.');
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const text = await res.text().catch(() => '');
|
|
64
|
+
throw new Error(`API error (${res.status}): ${text || res.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (res.status === 204) return null;
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const api = {
|
|
72
|
+
get: (path) => request('GET', path),
|
|
73
|
+
post: (path, body) => request('POST', path, body),
|
|
74
|
+
put: (path, body) => request('PUT', path, body),
|
|
75
|
+
patch: (path, body) => request('PATCH', path, body),
|
|
76
|
+
delete: (path) => request('DELETE', path),
|
|
77
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { allTools } from './tools.js';
|
|
10
|
+
|
|
11
|
+
const server = new Server(
|
|
12
|
+
{ name: 'directify', version: '1.0.0' },
|
|
13
|
+
{ capabilities: { tools: {} } }
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// List available tools
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
18
|
+
tools: allTools.map((tool) => ({
|
|
19
|
+
name: tool.name,
|
|
20
|
+
description: tool.description,
|
|
21
|
+
inputSchema: tool.inputSchema,
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Handle tool calls
|
|
26
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
27
|
+
const { name, arguments: args } = request.params;
|
|
28
|
+
const tool = allTools.find((t) => t.name === name);
|
|
29
|
+
|
|
30
|
+
if (!tool) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await tool.handler(args || {});
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: JSON.stringify(result, null, 2),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const transport = new StdioServerTransport();
|
|
56
|
+
await server.connect(transport);
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { api, resolveDirectory } from './api.js';
|
|
2
|
+
|
|
3
|
+
// ─── Directories ───
|
|
4
|
+
|
|
5
|
+
export const listDirectories = {
|
|
6
|
+
name: 'list_directories',
|
|
7
|
+
description: 'List all directories owned by the authenticated user.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {},
|
|
11
|
+
},
|
|
12
|
+
handler: async () => {
|
|
13
|
+
const data = await api.get('/directories');
|
|
14
|
+
return data.data || data;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ─── Categories ───
|
|
19
|
+
|
|
20
|
+
export const listCategories = {
|
|
21
|
+
name: 'list_categories',
|
|
22
|
+
description: 'List all active categories in a directory.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
directory_id: { type: 'string', description: 'Directory ID (optional if DIRECTIFY_DIRECTORY_ID is set)' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
handler: async ({ directory_id }) => {
|
|
30
|
+
const dir = resolveDirectory(directory_id);
|
|
31
|
+
const data = await api.get(`/directories/${dir}/categories`);
|
|
32
|
+
return data.data || data;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getCategory = {
|
|
37
|
+
name: 'get_category',
|
|
38
|
+
description: 'Get details of a specific category.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
43
|
+
category_id: { type: 'string', description: 'Category ID' },
|
|
44
|
+
},
|
|
45
|
+
required: ['category_id'],
|
|
46
|
+
},
|
|
47
|
+
handler: async ({ directory_id, category_id }) => {
|
|
48
|
+
const dir = resolveDirectory(directory_id);
|
|
49
|
+
const data = await api.get(`/directories/${dir}/categories/${category_id}`);
|
|
50
|
+
return data.data || data;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const createCategory = {
|
|
55
|
+
name: 'create_category',
|
|
56
|
+
description: 'Create a new category in a directory.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
61
|
+
title: { type: 'string', description: 'Category title (required)' },
|
|
62
|
+
slug: { type: 'string', description: 'URL slug (auto-generated if not set)' },
|
|
63
|
+
description: { type: 'string', description: 'Category description' },
|
|
64
|
+
content: { type: 'string', description: 'Category content (markdown)' },
|
|
65
|
+
icon: { type: 'string', description: 'Icon (emoji or URL)' },
|
|
66
|
+
custom_icon_url: { type: 'string', description: 'Custom icon image URL' },
|
|
67
|
+
parent_id: { type: 'number', description: 'Parent category ID for nesting' },
|
|
68
|
+
is_active: { type: 'boolean', description: 'Whether the category is active (default: true)' },
|
|
69
|
+
order: { type: 'number', description: 'Sort order' },
|
|
70
|
+
},
|
|
71
|
+
required: ['title'],
|
|
72
|
+
},
|
|
73
|
+
handler: async ({ directory_id, ...body }) => {
|
|
74
|
+
const dir = resolveDirectory(directory_id);
|
|
75
|
+
const data = await api.post(`/directories/${dir}/categories`, body);
|
|
76
|
+
return data.data || data;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const updateCategory = {
|
|
81
|
+
name: 'update_category',
|
|
82
|
+
description: 'Update an existing category.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
87
|
+
category_id: { type: 'string', description: 'Category ID to update' },
|
|
88
|
+
title: { type: 'string', description: 'Category title' },
|
|
89
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
90
|
+
description: { type: 'string', description: 'Category description' },
|
|
91
|
+
content: { type: 'string', description: 'Category content (markdown)' },
|
|
92
|
+
icon: { type: 'string', description: 'Icon' },
|
|
93
|
+
parent_id: { type: 'number', description: 'Parent category ID' },
|
|
94
|
+
is_active: { type: 'boolean', description: 'Active status' },
|
|
95
|
+
order: { type: 'number', description: 'Sort order' },
|
|
96
|
+
},
|
|
97
|
+
required: ['category_id'],
|
|
98
|
+
},
|
|
99
|
+
handler: async ({ directory_id, category_id, ...body }) => {
|
|
100
|
+
const dir = resolveDirectory(directory_id);
|
|
101
|
+
const data = await api.put(`/directories/${dir}/categories/${category_id}`, body);
|
|
102
|
+
return data.data || data;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const deleteCategory = {
|
|
107
|
+
name: 'delete_category',
|
|
108
|
+
description: 'Delete a category from a directory.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
113
|
+
category_id: { type: 'string', description: 'Category ID to delete' },
|
|
114
|
+
},
|
|
115
|
+
required: ['category_id'],
|
|
116
|
+
},
|
|
117
|
+
handler: async ({ directory_id, category_id }) => {
|
|
118
|
+
const dir = resolveDirectory(directory_id);
|
|
119
|
+
await api.delete(`/directories/${dir}/categories/${category_id}`);
|
|
120
|
+
return { success: true, message: `Category ${category_id} deleted.` };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ─── Tags ───
|
|
125
|
+
|
|
126
|
+
export const listTags = {
|
|
127
|
+
name: 'list_tags',
|
|
128
|
+
description: 'List all active tags in a directory.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
handler: async ({ directory_id }) => {
|
|
136
|
+
const dir = resolveDirectory(directory_id);
|
|
137
|
+
const data = await api.get(`/directories/${dir}/tags`);
|
|
138
|
+
return data.data || data;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const getTag = {
|
|
143
|
+
name: 'get_tag',
|
|
144
|
+
description: 'Get details of a specific tag.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
149
|
+
tag_id: { type: 'string', description: 'Tag ID' },
|
|
150
|
+
},
|
|
151
|
+
required: ['tag_id'],
|
|
152
|
+
},
|
|
153
|
+
handler: async ({ directory_id, tag_id }) => {
|
|
154
|
+
const dir = resolveDirectory(directory_id);
|
|
155
|
+
const data = await api.get(`/directories/${dir}/tags/${tag_id}`);
|
|
156
|
+
return data.data || data;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const createTag = {
|
|
161
|
+
name: 'create_tag',
|
|
162
|
+
description: 'Create a new tag in a directory.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
167
|
+
title: { type: 'string', description: 'Tag title (required)' },
|
|
168
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
169
|
+
color: { type: 'string', description: 'Background color (hex, e.g. #f59e0b)' },
|
|
170
|
+
text_color: { type: 'string', description: 'Text color (hex)' },
|
|
171
|
+
icon: { type: 'string', description: 'Icon' },
|
|
172
|
+
heroicon: { type: 'string', description: 'Heroicon name' },
|
|
173
|
+
is_active: { type: 'boolean', description: 'Active status (default: true)' },
|
|
174
|
+
},
|
|
175
|
+
required: ['title'],
|
|
176
|
+
},
|
|
177
|
+
handler: async ({ directory_id, ...body }) => {
|
|
178
|
+
const dir = resolveDirectory(directory_id);
|
|
179
|
+
const data = await api.post(`/directories/${dir}/tags`, body);
|
|
180
|
+
return data.data || data;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const updateTag = {
|
|
185
|
+
name: 'update_tag',
|
|
186
|
+
description: 'Update an existing tag.',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
191
|
+
tag_id: { type: 'string', description: 'Tag ID to update' },
|
|
192
|
+
title: { type: 'string', description: 'Tag title' },
|
|
193
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
194
|
+
color: { type: 'string', description: 'Background color (hex)' },
|
|
195
|
+
text_color: { type: 'string', description: 'Text color (hex)' },
|
|
196
|
+
is_active: { type: 'boolean', description: 'Active status' },
|
|
197
|
+
},
|
|
198
|
+
required: ['tag_id'],
|
|
199
|
+
},
|
|
200
|
+
handler: async ({ directory_id, tag_id, ...body }) => {
|
|
201
|
+
const dir = resolveDirectory(directory_id);
|
|
202
|
+
const data = await api.put(`/directories/${dir}/tags/${tag_id}`, body);
|
|
203
|
+
return data.data || data;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const deleteTag = {
|
|
208
|
+
name: 'delete_tag',
|
|
209
|
+
description: 'Delete a tag from a directory.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
214
|
+
tag_id: { type: 'string', description: 'Tag ID to delete' },
|
|
215
|
+
},
|
|
216
|
+
required: ['tag_id'],
|
|
217
|
+
},
|
|
218
|
+
handler: async ({ directory_id, tag_id }) => {
|
|
219
|
+
const dir = resolveDirectory(directory_id);
|
|
220
|
+
await api.delete(`/directories/${dir}/tags/${tag_id}`);
|
|
221
|
+
return { success: true, message: `Tag ${tag_id} deleted.` };
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ─── Custom Fields ───
|
|
226
|
+
|
|
227
|
+
export const listCustomFields = {
|
|
228
|
+
name: 'list_custom_fields',
|
|
229
|
+
description: 'List all custom fields defined for a directory. Use this to know which field names to set when creating/updating listings.',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
handler: async ({ directory_id }) => {
|
|
237
|
+
const dir = resolveDirectory(directory_id);
|
|
238
|
+
const data = await api.get(`/directories/${dir}/custom-fields`);
|
|
239
|
+
return data.data || data;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// ─── Listings ───
|
|
244
|
+
|
|
245
|
+
export const listListings = {
|
|
246
|
+
name: 'list_listings',
|
|
247
|
+
description: 'List all listings in a directory (paginated, 100 per page).',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
252
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
handler: async ({ directory_id, page = 1 }) => {
|
|
256
|
+
const dir = resolveDirectory(directory_id);
|
|
257
|
+
const data = await api.get(`/directories/${dir}/projects?page=${page}`);
|
|
258
|
+
return data;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const getListing = {
|
|
263
|
+
name: 'get_listing',
|
|
264
|
+
description: 'Get full details of a specific listing, including custom field values.',
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
269
|
+
listing_id: { type: 'string', description: 'Listing ID' },
|
|
270
|
+
},
|
|
271
|
+
required: ['listing_id'],
|
|
272
|
+
},
|
|
273
|
+
handler: async ({ directory_id, listing_id }) => {
|
|
274
|
+
const dir = resolveDirectory(directory_id);
|
|
275
|
+
const data = await api.get(`/directories/${dir}/projects/${listing_id}`);
|
|
276
|
+
return data.data || data;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const createListing = {
|
|
281
|
+
name: 'create_listing',
|
|
282
|
+
description:
|
|
283
|
+
'Create a new listing in a directory. You can include custom field values by using the field name as a key (use list_custom_fields to see available fields).',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
288
|
+
name: { type: 'string', description: 'Listing name (required)' },
|
|
289
|
+
url: { type: 'string', description: 'Website URL' },
|
|
290
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
291
|
+
description: { type: 'string', description: 'Short description' },
|
|
292
|
+
content: { type: 'string', description: 'Full content (markdown)' },
|
|
293
|
+
image_url: { type: 'string', description: 'Cover image URL' },
|
|
294
|
+
logo_url: { type: 'string', description: 'Logo URL' },
|
|
295
|
+
phone_number: { type: 'string', description: 'Phone number' },
|
|
296
|
+
email: { type: 'string', description: 'Email address' },
|
|
297
|
+
address: { type: 'string', description: 'Physical address' },
|
|
298
|
+
latitude: { type: 'number', description: 'Latitude' },
|
|
299
|
+
longitude: { type: 'number', description: 'Longitude' },
|
|
300
|
+
categories: {
|
|
301
|
+
type: 'array',
|
|
302
|
+
items: { type: 'number' },
|
|
303
|
+
description: 'Array of category IDs',
|
|
304
|
+
},
|
|
305
|
+
tags: {
|
|
306
|
+
type: 'array',
|
|
307
|
+
items: { type: 'number' },
|
|
308
|
+
description: 'Array of tag IDs',
|
|
309
|
+
},
|
|
310
|
+
is_active: { type: 'boolean', description: 'Active status (default: true)' },
|
|
311
|
+
is_featured: { type: 'boolean', description: 'Featured status' },
|
|
312
|
+
custom_fields: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
description: 'Custom field values as key-value pairs (e.g. {"price_range": "2", "cuisine_type": "Italian"})',
|
|
315
|
+
additionalProperties: true,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ['name'],
|
|
319
|
+
},
|
|
320
|
+
handler: async ({ directory_id, custom_fields, ...body }) => {
|
|
321
|
+
const dir = resolveDirectory(directory_id);
|
|
322
|
+
const payload = { ...body, ...custom_fields };
|
|
323
|
+
const data = await api.post(`/directories/${dir}/projects`, payload);
|
|
324
|
+
return data.data || data;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const updateListing = {
|
|
329
|
+
name: 'update_listing',
|
|
330
|
+
description: 'Update an existing listing. Only pass fields you want to change.',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
335
|
+
listing_id: { type: 'string', description: 'Listing ID to update' },
|
|
336
|
+
name: { type: 'string', description: 'Listing name' },
|
|
337
|
+
url: { type: 'string', description: 'Website URL' },
|
|
338
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
339
|
+
description: { type: 'string', description: 'Short description' },
|
|
340
|
+
content: { type: 'string', description: 'Full content (markdown)' },
|
|
341
|
+
image_url: { type: 'string', description: 'Cover image URL' },
|
|
342
|
+
logo_url: { type: 'string', description: 'Logo URL' },
|
|
343
|
+
phone_number: { type: 'string', description: 'Phone number' },
|
|
344
|
+
email: { type: 'string', description: 'Email address' },
|
|
345
|
+
address: { type: 'string', description: 'Physical address' },
|
|
346
|
+
latitude: { type: 'number', description: 'Latitude' },
|
|
347
|
+
longitude: { type: 'number', description: 'Longitude' },
|
|
348
|
+
categories: {
|
|
349
|
+
type: 'array',
|
|
350
|
+
items: { type: 'number' },
|
|
351
|
+
description: 'Array of category IDs',
|
|
352
|
+
},
|
|
353
|
+
tags: {
|
|
354
|
+
type: 'array',
|
|
355
|
+
items: { type: 'number' },
|
|
356
|
+
description: 'Array of tag IDs',
|
|
357
|
+
},
|
|
358
|
+
is_active: { type: 'boolean', description: 'Active status' },
|
|
359
|
+
is_featured: { type: 'boolean', description: 'Featured status' },
|
|
360
|
+
custom_fields: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
description: 'Custom field values as key-value pairs',
|
|
363
|
+
additionalProperties: true,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
required: ['listing_id'],
|
|
367
|
+
},
|
|
368
|
+
handler: async ({ directory_id, listing_id, custom_fields, ...body }) => {
|
|
369
|
+
const dir = resolveDirectory(directory_id);
|
|
370
|
+
const payload = { ...body, ...custom_fields };
|
|
371
|
+
const data = await api.put(`/directories/${dir}/projects/${listing_id}`, payload);
|
|
372
|
+
return data.data || data;
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export const deleteListing = {
|
|
377
|
+
name: 'delete_listing',
|
|
378
|
+
description: 'Delete a listing from a directory.',
|
|
379
|
+
inputSchema: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
properties: {
|
|
382
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
383
|
+
listing_id: { type: 'string', description: 'Listing ID to delete' },
|
|
384
|
+
},
|
|
385
|
+
required: ['listing_id'],
|
|
386
|
+
},
|
|
387
|
+
handler: async ({ directory_id, listing_id }) => {
|
|
388
|
+
const dir = resolveDirectory(directory_id);
|
|
389
|
+
await api.delete(`/directories/${dir}/projects/${listing_id}`);
|
|
390
|
+
return { success: true, message: `Listing ${listing_id} deleted.` };
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export const checkListingExists = {
|
|
395
|
+
name: 'check_listing_exists',
|
|
396
|
+
description: 'Check if a listing with a given URL already exists in the directory.',
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: {
|
|
400
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
401
|
+
url: { type: 'string', description: 'URL to check' },
|
|
402
|
+
},
|
|
403
|
+
required: ['url'],
|
|
404
|
+
},
|
|
405
|
+
handler: async ({ directory_id, url }) => {
|
|
406
|
+
const dir = resolveDirectory(directory_id);
|
|
407
|
+
try {
|
|
408
|
+
const data = await api.post(`/directories/${dir}/projects/exists`, { url });
|
|
409
|
+
return { exists: false, message: data.message };
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (err.message.includes('already exists')) {
|
|
412
|
+
return { exists: true, message: err.message };
|
|
413
|
+
}
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export const bulkCreateListings = {
|
|
420
|
+
name: 'bulk_create_listings',
|
|
421
|
+
description: 'Create multiple listings at once (max 100 per request).',
|
|
422
|
+
inputSchema: {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: {
|
|
425
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
426
|
+
listings: {
|
|
427
|
+
type: 'array',
|
|
428
|
+
description: 'Array of listing objects (each must have at least a "name" field)',
|
|
429
|
+
items: {
|
|
430
|
+
type: 'object',
|
|
431
|
+
properties: {
|
|
432
|
+
name: { type: 'string' },
|
|
433
|
+
url: { type: 'string' },
|
|
434
|
+
description: { type: 'string' },
|
|
435
|
+
categories: { type: 'array', items: { type: 'number' } },
|
|
436
|
+
tags: { type: 'array', items: { type: 'number' } },
|
|
437
|
+
},
|
|
438
|
+
required: ['name'],
|
|
439
|
+
additionalProperties: true,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required: ['listings'],
|
|
444
|
+
},
|
|
445
|
+
handler: async ({ directory_id, listings }) => {
|
|
446
|
+
const dir = resolveDirectory(directory_id);
|
|
447
|
+
const data = await api.post(`/directories/${dir}/projects/bulk`, { listings });
|
|
448
|
+
return data;
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// ─── Articles ───
|
|
453
|
+
|
|
454
|
+
export const listArticles = {
|
|
455
|
+
name: 'list_articles',
|
|
456
|
+
description: 'List all blog articles in a directory (paginated).',
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: {
|
|
460
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
461
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
handler: async ({ directory_id, page = 1 }) => {
|
|
465
|
+
const dir = resolveDirectory(directory_id);
|
|
466
|
+
const data = await api.get(`/directories/${dir}/articles?page=${page}`);
|
|
467
|
+
return data;
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export const getArticle = {
|
|
472
|
+
name: 'get_article',
|
|
473
|
+
description: 'Get full details of a specific article.',
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
478
|
+
article_id: { type: 'string', description: 'Article ID' },
|
|
479
|
+
},
|
|
480
|
+
required: ['article_id'],
|
|
481
|
+
},
|
|
482
|
+
handler: async ({ directory_id, article_id }) => {
|
|
483
|
+
const dir = resolveDirectory(directory_id);
|
|
484
|
+
const data = await api.get(`/directories/${dir}/articles/${article_id}`);
|
|
485
|
+
return data.data || data;
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
export const createArticle = {
|
|
490
|
+
name: 'create_article',
|
|
491
|
+
description: 'Create a new blog article. Provide either "content" (HTML) or "markdown" for the body.',
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: 'object',
|
|
494
|
+
properties: {
|
|
495
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
496
|
+
title: { type: 'string', description: 'Article title (required)' },
|
|
497
|
+
slug: { type: 'string', description: 'URL slug (auto-generated from title if not set)' },
|
|
498
|
+
content: { type: 'string', description: 'HTML content' },
|
|
499
|
+
markdown: { type: 'string', description: 'Markdown content' },
|
|
500
|
+
active: { type: 'boolean', description: 'Published status (default: true)' },
|
|
501
|
+
thumbnail_url: { type: 'string', description: 'Thumbnail image URL' },
|
|
502
|
+
categories: {
|
|
503
|
+
type: 'array',
|
|
504
|
+
items: { type: 'string' },
|
|
505
|
+
description: 'Category names (strings, not IDs)',
|
|
506
|
+
},
|
|
507
|
+
seo_title: { type: 'string', description: 'SEO title' },
|
|
508
|
+
seo_description: { type: 'string', description: 'SEO meta description' },
|
|
509
|
+
},
|
|
510
|
+
required: ['title'],
|
|
511
|
+
},
|
|
512
|
+
handler: async ({ directory_id, seo_title, seo_description, ...body }) => {
|
|
513
|
+
const dir = resolveDirectory(directory_id);
|
|
514
|
+
if (seo_title || seo_description) {
|
|
515
|
+
body.seo = {};
|
|
516
|
+
if (seo_title) body.seo.title = seo_title;
|
|
517
|
+
if (seo_description) body.seo.description = seo_description;
|
|
518
|
+
}
|
|
519
|
+
const data = await api.post(`/directories/${dir}/articles`, body);
|
|
520
|
+
return data.data || data;
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const updateArticle = {
|
|
525
|
+
name: 'update_article',
|
|
526
|
+
description: 'Update an existing article. Only pass fields you want to change.',
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: {
|
|
530
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
531
|
+
article_id: { type: 'string', description: 'Article ID to update' },
|
|
532
|
+
title: { type: 'string', description: 'Article title' },
|
|
533
|
+
slug: { type: 'string', description: 'URL slug' },
|
|
534
|
+
content: { type: 'string', description: 'HTML content' },
|
|
535
|
+
markdown: { type: 'string', description: 'Markdown content' },
|
|
536
|
+
active: { type: 'boolean', description: 'Published status' },
|
|
537
|
+
thumbnail_url: { type: 'string', description: 'Thumbnail image URL' },
|
|
538
|
+
categories: {
|
|
539
|
+
type: 'array',
|
|
540
|
+
items: { type: 'string' },
|
|
541
|
+
description: 'Category names',
|
|
542
|
+
},
|
|
543
|
+
seo_title: { type: 'string', description: 'SEO title' },
|
|
544
|
+
seo_description: { type: 'string', description: 'SEO meta description' },
|
|
545
|
+
},
|
|
546
|
+
required: ['article_id'],
|
|
547
|
+
},
|
|
548
|
+
handler: async ({ directory_id, article_id, seo_title, seo_description, ...body }) => {
|
|
549
|
+
const dir = resolveDirectory(directory_id);
|
|
550
|
+
if (seo_title || seo_description) {
|
|
551
|
+
body.seo = {};
|
|
552
|
+
if (seo_title) body.seo.title = seo_title;
|
|
553
|
+
if (seo_description) body.seo.description = seo_description;
|
|
554
|
+
}
|
|
555
|
+
const data = await api.put(`/directories/${dir}/articles/${article_id}`, body);
|
|
556
|
+
return data.data || data;
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
export const deleteArticle = {
|
|
561
|
+
name: 'delete_article',
|
|
562
|
+
description: 'Delete an article from a directory.',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
567
|
+
article_id: { type: 'string', description: 'Article ID to delete' },
|
|
568
|
+
},
|
|
569
|
+
required: ['article_id'],
|
|
570
|
+
},
|
|
571
|
+
handler: async ({ directory_id, article_id }) => {
|
|
572
|
+
const dir = resolveDirectory(directory_id);
|
|
573
|
+
await api.delete(`/directories/${dir}/articles/${article_id}`);
|
|
574
|
+
return { success: true, message: `Article ${article_id} deleted.` };
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export const toggleArticle = {
|
|
579
|
+
name: 'toggle_article',
|
|
580
|
+
description: 'Toggle an article between active and inactive status.',
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: 'object',
|
|
583
|
+
properties: {
|
|
584
|
+
directory_id: { type: 'string', description: 'Directory ID' },
|
|
585
|
+
article_id: { type: 'string', description: 'Article ID to toggle' },
|
|
586
|
+
},
|
|
587
|
+
required: ['article_id'],
|
|
588
|
+
},
|
|
589
|
+
handler: async ({ directory_id, article_id }) => {
|
|
590
|
+
const dir = resolveDirectory(directory_id);
|
|
591
|
+
const data = await api.patch(`/directories/${dir}/articles/${article_id}/toggle`);
|
|
592
|
+
return data.data || data;
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// ─── Export all tools ───
|
|
597
|
+
|
|
598
|
+
export const allTools = [
|
|
599
|
+
listDirectories,
|
|
600
|
+
listCategories,
|
|
601
|
+
getCategory,
|
|
602
|
+
createCategory,
|
|
603
|
+
updateCategory,
|
|
604
|
+
deleteCategory,
|
|
605
|
+
listTags,
|
|
606
|
+
getTag,
|
|
607
|
+
createTag,
|
|
608
|
+
updateTag,
|
|
609
|
+
deleteTag,
|
|
610
|
+
listCustomFields,
|
|
611
|
+
listListings,
|
|
612
|
+
getListing,
|
|
613
|
+
createListing,
|
|
614
|
+
updateListing,
|
|
615
|
+
deleteListing,
|
|
616
|
+
checkListingExists,
|
|
617
|
+
bulkCreateListings,
|
|
618
|
+
listArticles,
|
|
619
|
+
getArticle,
|
|
620
|
+
createArticle,
|
|
621
|
+
updateArticle,
|
|
622
|
+
deleteArticle,
|
|
623
|
+
toggleArticle,
|
|
624
|
+
];
|