@n0ml1/farm-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.
@@ -0,0 +1,32 @@
1
+ export class ApiClient {
2
+ baseUrl;
3
+ apiKey;
4
+ constructor(baseUrl, apiKey) {
5
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
6
+ this.apiKey = apiKey;
7
+ }
8
+ async request(method, path, body) {
9
+ const headers = {};
10
+ if (this.apiKey) {
11
+ headers["Authorization"] = this.apiKey;
12
+ }
13
+ if (body !== undefined) {
14
+ headers["Content-Type"] = "application/json";
15
+ }
16
+ const response = await fetch(`${this.baseUrl}${path}`, {
17
+ method,
18
+ headers,
19
+ body: body === undefined ? undefined : JSON.stringify(body),
20
+ });
21
+ if (response.status === 204) {
22
+ return undefined;
23
+ }
24
+ const text = await response.text();
25
+ const data = text ? JSON.parse(text) : undefined;
26
+ if (!response.ok) {
27
+ const errorMessage = data?.error || data?.message || response.statusText;
28
+ throw new Error(`${response.status} ${errorMessage}`);
29
+ }
30
+ return data;
31
+ }
32
+ }
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ApiClient } from "./api/client.js";
5
+ import { createServer } from "./server.js";
6
+ const parseCliArgs = (argv) => {
7
+ const options = {};
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const arg = argv[i];
10
+ if (arg === "--baseUrl" || arg === "-b") {
11
+ const value = argv[i + 1];
12
+ if (value) {
13
+ options.baseUrl = value;
14
+ i += 1;
15
+ }
16
+ continue;
17
+ }
18
+ if (arg.startsWith("--baseUrl=")) {
19
+ options.baseUrl = arg.slice("--baseUrl=".length) || undefined;
20
+ continue;
21
+ }
22
+ if (arg === "--apiKey" || arg === "-k") {
23
+ const value = argv[i + 1];
24
+ if (value) {
25
+ options.apiKey = value;
26
+ i += 1;
27
+ }
28
+ continue;
29
+ }
30
+ if (arg.startsWith("--apiKey=")) {
31
+ options.apiKey = arg.slice("--apiKey=".length) || undefined;
32
+ continue;
33
+ }
34
+ }
35
+ return options;
36
+ };
37
+ const cliOptions = parseCliArgs(process.argv.slice(2));
38
+ const baseUrl = cliOptions.baseUrl ??
39
+ process.env.IOS_FARM_API_URL ??
40
+ "http://localhost:3000";
41
+ const apiKey = cliOptions.apiKey ?? process.env.IOS_FARM_API_KEY;
42
+ const client = new ApiClient(baseUrl, apiKey);
43
+ const server = createServer(client);
44
+ const transport = new StdioServerTransport();
45
+ await server.connect(transport);
package/dist/server.js ADDED
@@ -0,0 +1,55 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { accountsTools } from "./tools/accounts.js";
4
+ import { scenariosTools } from "./tools/scenarios.js";
5
+ const tools = [...accountsTools, ...scenariosTools];
6
+ export function createServer(client) {
7
+ const server = new Server({
8
+ name: "farm-mcp",
9
+ version: "1.0.0",
10
+ }, {
11
+ capabilities: {
12
+ tools: {},
13
+ },
14
+ });
15
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
16
+ return {
17
+ tools: tools.map(({ name, description, inputSchema }) => ({
18
+ name,
19
+ description,
20
+ inputSchema,
21
+ })),
22
+ };
23
+ });
24
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
25
+ const toolName = request.params.name;
26
+ const tool = tools.find(item => item.name === toolName);
27
+ if (!tool) {
28
+ throw new Error(`Unknown tool: ${toolName}`);
29
+ }
30
+ try {
31
+ const result = await tool.handler(client, request.params.arguments ?? {});
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: JSON.stringify(result ?? { success: true }, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: JSON.stringify({ success: false, error: message }, null, 2),
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ });
54
+ return server;
55
+ }
@@ -0,0 +1,155 @@
1
+ import { z } from "zod";
2
+ const registerSchema = z.object({
3
+ platform: z.string(),
4
+ username: z.string(),
5
+ deviceId: z.string(),
6
+ metadata: z.record(z.unknown()).optional(),
7
+ });
8
+ const listSchema = z.object({
9
+ platform: z.string().optional(),
10
+ });
11
+ const getSchema = z.object({
12
+ id: z.string(),
13
+ });
14
+ const getByUsernameSchema = z.object({
15
+ platform: z.string(),
16
+ username: z.string(),
17
+ });
18
+ const getByDeviceSchema = z.object({
19
+ deviceId: z.string(),
20
+ });
21
+ const updateSchema = z.object({
22
+ id: z.string(),
23
+ username: z.string().optional(),
24
+ deviceId: z.string().optional(),
25
+ metadata: z.record(z.unknown()).optional(),
26
+ });
27
+ const deleteSchema = z.object({
28
+ id: z.string(),
29
+ });
30
+ const accountHandlers = {
31
+ async register(client, args) {
32
+ const input = registerSchema.parse(args);
33
+ return client.request("POST", "/accounts", input);
34
+ },
35
+ async list(client, args) {
36
+ const input = listSchema.parse(args);
37
+ const query = input.platform
38
+ ? `?platform=${encodeURIComponent(input.platform)}`
39
+ : "";
40
+ return client.request("GET", `/accounts${query}`);
41
+ },
42
+ async get(client, args) {
43
+ const input = getSchema.parse(args);
44
+ return client.request("GET", `/accounts/${encodeURIComponent(input.id)}`);
45
+ },
46
+ async getByUsername(client, args) {
47
+ const input = getByUsernameSchema.parse(args);
48
+ return client.request("GET", `/accounts/by-username/${encodeURIComponent(input.platform)}/${encodeURIComponent(input.username)}`);
49
+ },
50
+ async getByDevice(client, args) {
51
+ const input = getByDeviceSchema.parse(args);
52
+ return client.request("GET", `/accounts/by-device/${encodeURIComponent(input.deviceId)}`);
53
+ },
54
+ async update(client, args) {
55
+ const input = updateSchema.parse(args);
56
+ const { id, ...payload } = input;
57
+ return client.request("PUT", `/accounts/${encodeURIComponent(id)}`, payload);
58
+ },
59
+ async remove(client, args) {
60
+ const input = deleteSchema.parse(args);
61
+ return client.request("DELETE", `/accounts/${encodeURIComponent(input.id)}`);
62
+ },
63
+ };
64
+ export const accountsTools = [
65
+ {
66
+ name: "accounts_register",
67
+ description: "Register a new account",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ platform: { type: "string" },
72
+ username: { type: "string" },
73
+ deviceId: { type: "string" },
74
+ metadata: { type: "object", additionalProperties: true },
75
+ },
76
+ required: ["platform", "username", "deviceId"],
77
+ },
78
+ handler: accountHandlers.register,
79
+ },
80
+ {
81
+ name: "accounts_list",
82
+ description: "List accounts, optionally filtered by platform",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ platform: { type: "string" },
87
+ },
88
+ },
89
+ handler: accountHandlers.list,
90
+ },
91
+ {
92
+ name: "accounts_get",
93
+ description: "Get account by id",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ id: { type: "string" },
98
+ },
99
+ required: ["id"],
100
+ },
101
+ handler: accountHandlers.get,
102
+ },
103
+ {
104
+ name: "accounts_get_by_username",
105
+ description: "Get account by platform and username",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ platform: { type: "string" },
110
+ username: { type: "string" },
111
+ },
112
+ required: ["platform", "username"],
113
+ },
114
+ handler: accountHandlers.getByUsername,
115
+ },
116
+ {
117
+ name: "accounts_get_by_device",
118
+ description: "Get accounts by device id",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ deviceId: { type: "string" },
123
+ },
124
+ required: ["deviceId"],
125
+ },
126
+ handler: accountHandlers.getByDevice,
127
+ },
128
+ {
129
+ name: "accounts_update",
130
+ description: "Update account fields",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ id: { type: "string" },
135
+ username: { type: "string" },
136
+ deviceId: { type: "string" },
137
+ metadata: { type: "object", additionalProperties: true },
138
+ },
139
+ required: ["id"],
140
+ },
141
+ handler: accountHandlers.update,
142
+ },
143
+ {
144
+ name: "accounts_delete",
145
+ description: "Delete account by id",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ id: { type: "string" },
150
+ },
151
+ required: ["id"],
152
+ },
153
+ handler: accountHandlers.remove,
154
+ },
155
+ ];
@@ -0,0 +1,219 @@
1
+ import { z } from "zod";
2
+ const uploadContentSchema = z.object({
3
+ accountIds: z.array(z.string()).min(1),
4
+ description: z.string(),
5
+ videoUrl: z.string().url(),
6
+ });
7
+ const carouselUploadSchema = z.object({
8
+ accountIds: z.array(z.string()).min(1),
9
+ description: z.string(),
10
+ imageUrls: z.array(z.string().url()).min(1),
11
+ });
12
+ const forYouWatchSchema = z.object({
13
+ accountIds: z.array(z.string()).min(1),
14
+ totalWatchTimeMin: z.number().optional(),
15
+ totalWatchTimeMax: z.number().optional(),
16
+ videoWatchTimeMin: z.number().optional(),
17
+ videoWatchTimeMax: z.number().optional(),
18
+ likePercentage: z.number().optional(),
19
+ profileParams: z
20
+ .object({
21
+ percentage: z.number().optional(),
22
+ minDepth: z.number().optional(),
23
+ maxDepth: z.number().optional(),
24
+ })
25
+ .optional(),
26
+ });
27
+ const searchAndWatchSchema = forYouWatchSchema.extend({
28
+ keyword: z.string(),
29
+ });
30
+ const profileWatchSchema = forYouWatchSchema.extend({
31
+ profiles: z.array(z.string()).min(1),
32
+ });
33
+ const likeSchema = z.object({
34
+ accountId: z.string(),
35
+ });
36
+ const taskSchema = z.object({
37
+ taskId: z.string(),
38
+ });
39
+ const scenarioHandlers = {
40
+ async uploadContent(client, args) {
41
+ const input = uploadContentSchema.parse(args);
42
+ return client.request("POST", "/scenarios/upload", input);
43
+ },
44
+ async carouselUpload(client, args) {
45
+ const input = carouselUploadSchema.parse(args);
46
+ return client.request("POST", "/scenarios/carousel-upload", input);
47
+ },
48
+ async forYouWatch(client, args) {
49
+ const input = forYouWatchSchema.parse(args);
50
+ return client.request("POST", "/scenarios/for-you-watch", input);
51
+ },
52
+ async searchAndWatch(client, args) {
53
+ const input = searchAndWatchSchema.parse(args);
54
+ return client.request("POST", "/scenarios/search-and-watch", input);
55
+ },
56
+ async profileWatch(client, args) {
57
+ const input = profileWatchSchema.parse(args);
58
+ return client.request("POST", "/scenarios/profile-watch", input);
59
+ },
60
+ async like(client, args) {
61
+ const input = likeSchema.parse(args);
62
+ return client.request("POST", "/scenarios/like", input);
63
+ },
64
+ async getTask(client, args) {
65
+ const input = taskSchema.parse(args);
66
+ return client.request("GET", `/scenarios/task/${encodeURIComponent(input.taskId)}`);
67
+ },
68
+ async cancelTask(client, args) {
69
+ const input = taskSchema.parse(args);
70
+ return client.request("POST", `/scenarios/task/${encodeURIComponent(input.taskId)}/cancel`);
71
+ },
72
+ };
73
+ export const scenariosTools = [
74
+ {
75
+ name: "scenarios_upload_content",
76
+ description: "Upload content to platform using videoUrl",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ accountIds: { type: "array", items: { type: "string" } },
81
+ description: { type: "string" },
82
+ videoUrl: { type: "string", format: "uri" },
83
+ },
84
+ required: ["accountIds", "description", "videoUrl"],
85
+ },
86
+ handler: scenarioHandlers.uploadContent,
87
+ },
88
+ {
89
+ name: "scenarios_carousel_upload",
90
+ description: "Upload image carousel using imageUrls",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ accountIds: { type: "array", items: { type: "string" } },
95
+ description: { type: "string" },
96
+ imageUrls: {
97
+ type: "array",
98
+ items: { type: "string", format: "uri" },
99
+ },
100
+ },
101
+ required: ["accountIds", "description", "imageUrls"],
102
+ },
103
+ handler: scenarioHandlers.carouselUpload,
104
+ },
105
+ {
106
+ name: "scenarios_for_you_watch",
107
+ description: "Watch For You feed with optional parameters",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ accountIds: { type: "array", items: { type: "string" } },
112
+ totalWatchTimeMin: { type: "number" },
113
+ totalWatchTimeMax: { type: "number" },
114
+ videoWatchTimeMin: { type: "number" },
115
+ videoWatchTimeMax: { type: "number" },
116
+ likePercentage: { type: "number" },
117
+ profileParams: {
118
+ type: "object",
119
+ properties: {
120
+ percentage: { type: "number" },
121
+ minDepth: { type: "number" },
122
+ maxDepth: { type: "number" },
123
+ },
124
+ },
125
+ },
126
+ required: ["accountIds"],
127
+ },
128
+ handler: scenarioHandlers.forYouWatch,
129
+ },
130
+ {
131
+ name: "scenarios_search_and_watch",
132
+ description: "Search by keyword and watch videos",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ accountIds: { type: "array", items: { type: "string" } },
137
+ keyword: { type: "string" },
138
+ totalWatchTimeMin: { type: "number" },
139
+ totalWatchTimeMax: { type: "number" },
140
+ videoWatchTimeMin: { type: "number" },
141
+ videoWatchTimeMax: { type: "number" },
142
+ likePercentage: { type: "number" },
143
+ profileParams: {
144
+ type: "object",
145
+ properties: {
146
+ percentage: { type: "number" },
147
+ minDepth: { type: "number" },
148
+ maxDepth: { type: "number" },
149
+ },
150
+ },
151
+ },
152
+ required: ["accountIds", "keyword"],
153
+ },
154
+ handler: scenarioHandlers.searchAndWatch,
155
+ },
156
+ {
157
+ name: "scenarios_profile_watch",
158
+ description: "Open profiles and watch their videos",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ accountIds: { type: "array", items: { type: "string" } },
163
+ profiles: { type: "array", items: { type: "string" } },
164
+ totalWatchTimeMin: { type: "number" },
165
+ totalWatchTimeMax: { type: "number" },
166
+ videoWatchTimeMin: { type: "number" },
167
+ videoWatchTimeMax: { type: "number" },
168
+ likePercentage: { type: "number" },
169
+ profileParams: {
170
+ type: "object",
171
+ properties: {
172
+ percentage: { type: "number" },
173
+ minDepth: { type: "number" },
174
+ maxDepth: { type: "number" },
175
+ },
176
+ required: ["percentage", "minDepth", "maxDepth"],
177
+ },
178
+ },
179
+ required: ["accountIds", "profiles", "profileParams"],
180
+ },
181
+ handler: scenarioHandlers.profileWatch,
182
+ },
183
+ {
184
+ name: "scenarios_like",
185
+ description: "Like current content",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ accountId: { type: "string" },
190
+ },
191
+ required: ["accountId"],
192
+ },
193
+ handler: scenarioHandlers.like,
194
+ },
195
+ {
196
+ name: "scenarios_get_task",
197
+ description: "Get task status by taskId",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ taskId: { type: "string" },
202
+ },
203
+ required: ["taskId"],
204
+ },
205
+ handler: scenarioHandlers.getTask,
206
+ },
207
+ {
208
+ name: "scenarios_cancel_task",
209
+ description: "Cancel task by taskId",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ taskId: { type: "string" },
214
+ },
215
+ required: ["taskId"],
216
+ },
217
+ handler: scenarioHandlers.cancelTask,
218
+ },
219
+ ];
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@n0ml1/farm-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for iOS Farm accounts and scenarios",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "farm-mcp": "dist/index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [],
14
+ "author": "n0ml1",
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.0.4",
18
+ "dotenv": "^17.2.3",
19
+ "zod": "^3.25.76"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.2.0",
23
+ "tsx": "^4.20.5",
24
+ "typescript": "^5.7.3"
25
+ },
26
+ "scripts": {
27
+ "dev": "tsx src/index.ts",
28
+ "debug": "tsx --inspect-brk=9229 src/index.ts",
29
+ "build": "tsc -p tsconfig.build.json",
30
+ "start": "node dist/index.js"
31
+ }
32
+ }