@genseo/cli 0.1.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 ADDED
@@ -0,0 +1,166 @@
1
+ # Genseo CLI and MCP
2
+
3
+ This package provides:
4
+
5
+ - `genseo`: command-line client for the Genseo Public API
6
+ - `genseo-mcp`: stdio MCP server for Claude, Cursor, Codex, and other MCP clients
7
+
8
+ ## Install
9
+
10
+ After the package is published to npm:
11
+
12
+ ```bash
13
+ npm install -g @genseo/cli
14
+ ```
15
+
16
+ Or run without a global install:
17
+
18
+ ```bash
19
+ npx -y --package @genseo/cli genseo me
20
+ ```
21
+
22
+ Requirements:
23
+
24
+ - Node.js 20 or newer
25
+ - A Genseo project
26
+ - Either browser login with `genseo login` or a project-bound API key from the API / Developer settings tab
27
+
28
+ ## CLI
29
+
30
+ Login with the browser-based Device Flow:
31
+
32
+ ```bash
33
+ genseo login
34
+ ```
35
+
36
+ The CLI opens `https://app.genseo.co/developer/device`, waits for approval, then stores the returned project-bound API key in `~/.genseo/config.json`.
37
+
38
+ Manual API-key login is also supported:
39
+
40
+ ```bash
41
+ genseo login --api-key gs_live_...
42
+ ```
43
+
44
+ You can also use environment variables:
45
+
46
+ ```bash
47
+ export GENSEO_API_BASE=https://api.genseo.co/v1
48
+ export GENSEO_API_KEY=gs_live_...
49
+ ```
50
+
51
+ Common commands:
52
+
53
+ ```bash
54
+ genseo me
55
+ genseo project get
56
+ genseo project create --domain example.com --name "Example" --generate-keywords
57
+ genseo keywords list
58
+ genseo keywords add "seo automation software"
59
+ genseo keywords generate
60
+ genseo posts list
61
+ genseo posts create --title "SEO Automation Workflow"
62
+ genseo posts generate <postId>
63
+ genseo posts publish <postId>
64
+ genseo integrations list
65
+ genseo webhooks create --target-url https://example.com/hook
66
+ ```
67
+
68
+ ## MCP
69
+
70
+ Run the MCP server with a key after global install:
71
+
72
+ ```bash
73
+ GENSEO_API_KEY=gs_live_... GENSEO_API_BASE=https://api.genseo.co/v1 genseo-mcp
74
+ ```
75
+
76
+ Example MCP client config with global install:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "genseo": {
82
+ "command": "genseo-mcp",
83
+ "env": {
84
+ "GENSEO_API_BASE": "https://api.genseo.co/v1",
85
+ "GENSEO_API_KEY": "gs_live_..."
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ Example MCP client config without global install:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "genseo": {
98
+ "command": "npx",
99
+ "args": ["-y", "--package", "@genseo/cli", "genseo-mcp"],
100
+ "env": {
101
+ "GENSEO_API_BASE": "https://api.genseo.co/v1",
102
+ "GENSEO_API_KEY": "gs_live_..."
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Available tools include:
110
+
111
+ - `genseo_me`
112
+ - `genseo_project_get`
113
+ - `genseo_project_create`
114
+ - `genseo_keywords_list`
115
+ - `genseo_keywords_create`
116
+ - `genseo_keywords_generate`
117
+ - `genseo_keyword_metrics`
118
+ - `genseo_posts_list`
119
+ - `genseo_posts_create_draft`
120
+ - `genseo_posts_get`
121
+ - `genseo_posts_update`
122
+ - `genseo_posts_generate`
123
+ - `genseo_posts_publish`
124
+ - `genseo_integrations_list`
125
+ - `genseo_integration_fields`
126
+ - `genseo_integration_connect_url`
127
+ - `genseo_integration_mapping_save`
128
+ - `genseo_webhooks_list`
129
+ - `genseo_webhooks_create`
130
+ - `genseo_webhooks_delete`
131
+
132
+ Agents should call `genseo_me` first, use only the returned project, create drafts before publishing, and ask the user before calling `genseo_posts_publish` unless autonomous publishing is explicitly enabled.
133
+
134
+ ## Errors
135
+
136
+ The CLI prints API errors with their stable code, for example:
137
+
138
+ - `invalid_api_key`: missing, malformed, or unknown API key
139
+ - `project_forbidden`: API key does not belong to the requested project
140
+ - `insufficient_scope`: API key is missing the required scope
141
+ - `key_revoked`: API key was revoked
142
+ - `key_expired`: API key expired
143
+ - `resource_not_found`: requested resource is missing or belongs to another project
144
+ - `integration_not_configured`: publishing was requested but no publishing integration is connected
145
+
146
+ Agents should treat `403` errors as final, `429` errors with backoff, and `202` responses as async accepted.
147
+
148
+ ## Publishing This Package
149
+
150
+ Release checklist for Genseo maintainers:
151
+
152
+ 1. Confirm production API routing works:
153
+ `https://api.genseo.co/v1/me` rewrites to the app's `/api/v1/me`.
154
+ 2. Confirm OAuth Device Flow routing works:
155
+ `https://api.genseo.co/oauth/device/code` rewrites to `/api/oauth/device/code`.
156
+ 3. Set production environment variables:
157
+ `NEXT_PUBLIC_API_HOST=api.genseo.co`,
158
+ `NEXT_PUBLIC_API_URL=https://api.genseo.co/v1`,
159
+ `NEXT_PUBLIC_APP_URL=https://app.genseo.co`,
160
+ `PUBLIC_API_KEY_ENCRYPTION_KEY=...`.
161
+ 4. Run tests:
162
+ `npm run test` and `npm run test:cli` from the `web` workspace.
163
+ 5. Dry-run the npm package:
164
+ `npm pack --dry-run` from `web/packages/genseo-cli`.
165
+ 6. Publish:
166
+ `npm publish --access public` from `web/packages/genseo-cli`.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runMcpServer } from "../src/mcp-server.mjs";
3
+
4
+ runMcpServer().catch((error) => {
5
+ console.error(error instanceof Error ? error.stack || error.message : String(error));
6
+ process.exit(1);
7
+ });
package/bin/genseo.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli.mjs";
3
+
4
+ runCli(process.argv.slice(2)).catch((error) => {
5
+ console.error(error instanceof Error ? error.message : String(error));
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@genseo/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Genseo CLI and MCP server for the Genseo Public API.",
7
+ "license": "UNLICENSED",
8
+ "homepage": "https://genseo.co",
9
+ "bugs": {
10
+ "url": "https://genseo.co"
11
+ },
12
+ "keywords": [
13
+ "genseo",
14
+ "seo",
15
+ "cli",
16
+ "mcp",
17
+ "public-api"
18
+ ],
19
+ "bin": {
20
+ "genseo": "./bin/genseo.mjs",
21
+ "genseo-mcp": "./bin/genseo-mcp.mjs"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "src",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "test": "node tests/smoke.mjs"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ }
37
+ }
@@ -0,0 +1,175 @@
1
+ import { resolveConfig } from "./config.mjs";
2
+
3
+ export class GenseoApiError extends Error {
4
+ constructor(message, { status, code, details, response } = {}) {
5
+ super(message);
6
+ this.name = "GenseoApiError";
7
+ this.status = status;
8
+ this.code = code;
9
+ this.details = details;
10
+ this.response = response;
11
+ }
12
+ }
13
+
14
+ export async function createClient(overrides = {}) {
15
+ const config = await resolveConfig(overrides);
16
+ return new GenseoClient(config);
17
+ }
18
+
19
+ export class GenseoClient {
20
+ constructor(config) {
21
+ this.apiBase = config.apiBase;
22
+ this.authBase = config.authBase;
23
+ this.apiKey = config.apiKey;
24
+ }
25
+
26
+ async request(method, path, { body, query, auth = true } = {}) {
27
+ const url = new URL(`${this.apiBase}${path.startsWith("/") ? path : `/${path}`}`);
28
+ for (const [key, value] of Object.entries(query || {})) {
29
+ if (value !== undefined && value !== null && value !== "") {
30
+ url.searchParams.set(key, String(value));
31
+ }
32
+ }
33
+
34
+ const headers = { Accept: "application/json" };
35
+ if (body !== undefined) headers["Content-Type"] = "application/json";
36
+ if (auth) {
37
+ if (!this.apiKey) {
38
+ throw new GenseoApiError("Missing API key. Run `genseo login` or set GENSEO_API_KEY.", {
39
+ code: "missing_api_key",
40
+ });
41
+ }
42
+ headers.Authorization = `Bearer ${this.apiKey}`;
43
+ }
44
+
45
+ const response = await fetch(url, {
46
+ method,
47
+ headers,
48
+ body: body === undefined ? undefined : JSON.stringify(body),
49
+ });
50
+ const text = await response.text();
51
+ const data = text ? safeJson(text) : null;
52
+
53
+ if (!response.ok) {
54
+ const error = data?.error;
55
+ throw new GenseoApiError(error?.message || data?.error_description || response.statusText, {
56
+ status: response.status,
57
+ code: error?.code || data?.error || "request_failed",
58
+ details: error?.details || data,
59
+ response: data,
60
+ });
61
+ }
62
+
63
+ return data;
64
+ }
65
+
66
+ async authRequest(method, path, { body } = {}) {
67
+ const url = `${this.authBase}${path.startsWith("/") ? path : `/${path}`}`;
68
+ const response = await fetch(url, {
69
+ method,
70
+ headers: {
71
+ Accept: "application/json",
72
+ "Content-Type": "application/json",
73
+ },
74
+ body: body === undefined ? undefined : JSON.stringify(body),
75
+ });
76
+ const text = await response.text();
77
+ const data = text ? safeJson(text) : null;
78
+ if (!response.ok) {
79
+ throw new GenseoApiError(data?.error_description || data?.message || data?.error || response.statusText, {
80
+ status: response.status,
81
+ code: data?.error || "request_failed",
82
+ response: data,
83
+ });
84
+ }
85
+ return data;
86
+ }
87
+
88
+ me() {
89
+ return this.request("GET", "/me");
90
+ }
91
+
92
+ project(projectId) {
93
+ return projectId ? this.request("GET", `/projects/${projectId}`) : this.request("GET", "/project");
94
+ }
95
+
96
+ createProject(body) {
97
+ return this.request("POST", "/projects", { body });
98
+ }
99
+
100
+ listKeywords(projectId, options = {}) {
101
+ return this.request("GET", `/projects/${projectId}/keywords`, { query: { limit: options.limit } });
102
+ }
103
+
104
+ createKeyword(projectId, body) {
105
+ return this.request("POST", `/projects/${projectId}/keywords`, { body });
106
+ }
107
+
108
+ generateKeywords(projectId) {
109
+ return this.request("POST", `/projects/${projectId}/keywords/generate`, { body: {} });
110
+ }
111
+
112
+ keywordMetrics(projectId, keywords) {
113
+ return this.request("POST", `/projects/${projectId}/keyword-metrics`, { body: { keywords } });
114
+ }
115
+
116
+ listPosts(projectId, options = {}) {
117
+ return this.request("GET", `/projects/${projectId}/posts`, { query: { limit: options.limit } });
118
+ }
119
+
120
+ createPost(projectId, body) {
121
+ return this.request("POST", `/projects/${projectId}/posts`, { body });
122
+ }
123
+
124
+ getPost(projectId, postId) {
125
+ return this.request("GET", `/projects/${projectId}/posts/${postId}`);
126
+ }
127
+
128
+ updatePost(projectId, postId, body) {
129
+ return this.request("PATCH", `/projects/${projectId}/posts/${postId}`, { body });
130
+ }
131
+
132
+ generatePost(projectId, postId) {
133
+ return this.request("POST", `/projects/${projectId}/posts/${postId}/generate`, { body: {} });
134
+ }
135
+
136
+ publishPost(projectId, postId) {
137
+ return this.request("POST", `/projects/${projectId}/posts/${postId}/publish`, { body: {} });
138
+ }
139
+
140
+ listIntegrations(projectId) {
141
+ return this.request("GET", `/projects/${projectId}/integrations`);
142
+ }
143
+
144
+ integrationFields(projectId, provider) {
145
+ return this.request("GET", `/projects/${projectId}/integrations/${provider}/fields`);
146
+ }
147
+
148
+ integrationConnectUrl(projectId, provider) {
149
+ return this.request("POST", `/projects/${projectId}/integrations/${provider}/connect-url`, { body: {} });
150
+ }
151
+
152
+ saveIntegrationMapping(projectId, provider, body) {
153
+ return this.request("POST", `/projects/${projectId}/integrations/${provider}/mapping`, { body });
154
+ }
155
+
156
+ listWebhooks(projectId) {
157
+ return this.request("GET", `/projects/${projectId}/webhooks`);
158
+ }
159
+
160
+ createWebhook(projectId, body) {
161
+ return this.request("POST", `/projects/${projectId}/webhooks`, { body });
162
+ }
163
+
164
+ deleteWebhook(projectId, webhookId) {
165
+ return this.request("DELETE", `/projects/${projectId}/webhooks/${webhookId}`);
166
+ }
167
+ }
168
+
169
+ function safeJson(text) {
170
+ try {
171
+ return JSON.parse(text);
172
+ } catch {
173
+ return { raw: text };
174
+ }
175
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,354 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { clearStoredConfig, DEFAULT_API_BASE, resolveConfig, saveLogin } from "./config.mjs";
4
+ import { createClient, GenseoApiError } from "./api-client.mjs";
5
+
6
+ const DEFAULT_SCOPES = [
7
+ "projects:read",
8
+ "keywords:read",
9
+ "keywords:write",
10
+ "keywords:generate",
11
+ "posts:read",
12
+ "posts:write",
13
+ "posts:generate",
14
+ "posts:publish",
15
+ "integrations:read",
16
+ "integrations:write",
17
+ "webhooks:write",
18
+ ];
19
+
20
+ export async function runCli(argv) {
21
+ const parsed = parseArgs(argv);
22
+ if (parsed.flags.help || parsed.positionals.length === 0) return printHelp();
23
+ if (parsed.flags.version) return console.log("genseo 0.1.0");
24
+
25
+ const [command, subcommand, ...rest] = parsed.positionals;
26
+ const flags = parsed.flags;
27
+
28
+ try {
29
+ if (command === "login") return login(flags);
30
+ if (command === "logout") return logout();
31
+ if (command === "me") return printJson(await (await createClient(flagsToConfig(flags))).me());
32
+ if (command === "project") return project(subcommand, rest, flags);
33
+ if (command === "keywords") return keywords(subcommand, rest, flags);
34
+ if (command === "posts") return posts(subcommand, rest, flags);
35
+ if (command === "integrations") return integrations(subcommand, rest, flags);
36
+ if (command === "webhooks") return webhooks(subcommand, rest, flags);
37
+ } catch (error) {
38
+ if (error instanceof GenseoApiError) {
39
+ const suffix = error.code ? ` (${error.code})` : "";
40
+ throw new Error(`Genseo API error${suffix}: ${error.message}`);
41
+ }
42
+ throw error;
43
+ }
44
+
45
+ throw new Error(`Unknown command: ${command}`);
46
+ }
47
+
48
+ async function login(flags) {
49
+ const apiBase = flags["api-base"] || flags.apiBase || DEFAULT_API_BASE;
50
+ const config = await resolveConfig({ apiBase });
51
+ const authBase = flags["auth-base"] || flags.authBase || config.authBase;
52
+
53
+ if (flags["api-key"] || flags.apiKey) {
54
+ const apiKey = flags["api-key"] || flags.apiKey;
55
+ const client = await createClient({ apiBase, authBase, apiKey });
56
+ const me = await client.me();
57
+ await saveLogin({ apiBase: config.apiBase, authBase, apiKey });
58
+ console.log("API key saved.");
59
+ console.log(`Project: ${me.project.id}`);
60
+ return;
61
+ }
62
+
63
+ const client = await createClient({ apiBase, authBase, apiKey: "not-needed" });
64
+ const scopes = stringList(flags.scopes || flags.scope).length ? stringList(flags.scopes || flags.scope) : DEFAULT_SCOPES;
65
+ const start = await client.authRequest("POST", "/oauth/device/code", {
66
+ body: {
67
+ client_name: flags["client-name"] || flags.clientName || "Genseo CLI",
68
+ scopes,
69
+ project_id: flags["project-id"] || flags.projectId,
70
+ },
71
+ });
72
+
73
+ console.log(`Open this URL to authorize the CLI:\n${start.verification_uri}`);
74
+ console.log(`Code: ${start.user_code}`);
75
+ if (!flags["no-open"]) openBrowser(start.verification_uri);
76
+
77
+ const intervalMs = Math.max(Number(start.interval || 5), 1) * 1000;
78
+ const deadline = Date.now() + Math.max(Number(start.expires_in || 600), 60) * 1000;
79
+
80
+ while (Date.now() < deadline) {
81
+ await delay(intervalMs);
82
+ try {
83
+ const token = await client.authRequest("POST", "/oauth/device/token", {
84
+ body: {
85
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
86
+ device_code: start.device_code,
87
+ },
88
+ });
89
+ await saveLogin({
90
+ apiBase: token.api_base_url || config.apiBase,
91
+ authBase,
92
+ apiKey: token.access_token,
93
+ });
94
+ console.log("Login successful.");
95
+ console.log(`Project: ${token.project_id}`);
96
+ return;
97
+ } catch (error) {
98
+ if (error instanceof GenseoApiError && error.code === "authorization_pending") {
99
+ process.stdout.write(".");
100
+ continue;
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ throw new Error("Login expired. Run `genseo login` again.");
107
+ }
108
+
109
+ async function logout() {
110
+ await clearStoredConfig();
111
+ console.log("Logged out.");
112
+ }
113
+
114
+ async function project(subcommand, _rest, flags) {
115
+ const client = await createClient(flagsToConfig(flags));
116
+ const projectId = flags["project-id"] || flags.projectId;
117
+ if (!subcommand || subcommand === "get") return printJson(await client.project(projectId));
118
+ if (subcommand === "create") {
119
+ if (!flags.domain) throw new Error("Usage: genseo project create --domain example.com");
120
+ return printJson(await client.createProject(compact({
121
+ domain: flags.domain,
122
+ name: flags.name,
123
+ description: flags.description,
124
+ category: flags.category,
125
+ business_type: flags["business-type"] || flags.businessType,
126
+ region: flags.region,
127
+ language_code: flags["language-code"] || flags.languageCode || flags.language,
128
+ sitemap_url: flags["sitemap-url"] || flags.sitemapUrl,
129
+ blog_url: flags["blog-url"] || flags.blogUrl,
130
+ business_goal: flags["business-goal"] || flags.businessGoal,
131
+ faqs: jsonFlag(flags["faqs-json"] || flags.faqsJson),
132
+ competitors: jsonFlag(flags["competitors-json"] || flags.competitorsJson),
133
+ ctas: jsonFlag(flags["ctas-json"] || flags.ctasJson),
134
+ generate_keywords: booleanFlag(flags["generate-keywords"] || flags.generateKeywords),
135
+ issue_api_key: booleanFlag(flags["issue-api-key"] || flags.issueApiKey),
136
+ api_key_name: flags["api-key-name"] || flags.apiKeyName,
137
+ api_key_scopes: stringList(flags["api-key-scopes"] || flags.apiKeyScopes),
138
+ })));
139
+ }
140
+ throw new Error(`Unknown project command: ${subcommand}`);
141
+ }
142
+
143
+ async function keywords(subcommand, rest, flags) {
144
+ const client = await createClient(flagsToConfig(flags));
145
+ const projectId = await resolveProjectId(client, flags);
146
+ if (subcommand === "list") return printJson(await client.listKeywords(projectId, { limit: flags.limit }));
147
+ if (subcommand === "add") {
148
+ const keyword = rest.join(" ").trim() || flags.keyword;
149
+ if (!keyword) throw new Error("Usage: genseo keywords add <keyword>");
150
+ return printJson(await client.createKeyword(projectId, {
151
+ keyword,
152
+ volume: numberOrNull(flags.volume),
153
+ kd: numberOrNull(flags.kd),
154
+ intent: flags.intent,
155
+ source: flags.source || "cli",
156
+ }));
157
+ }
158
+ if (subcommand === "generate") return printJson(await client.generateKeywords(projectId));
159
+ if (subcommand === "metrics") {
160
+ const values = rest.length ? rest : stringList(flags.keywords);
161
+ if (!values.length) throw new Error("Usage: genseo keywords metrics <keyword...>");
162
+ return printJson(await client.keywordMetrics(projectId, values));
163
+ }
164
+ throw new Error(`Unknown keywords command: ${subcommand}`);
165
+ }
166
+
167
+ async function posts(subcommand, rest, flags) {
168
+ const client = await createClient(flagsToConfig(flags));
169
+ const projectId = await resolveProjectId(client, flags);
170
+ if (subcommand === "list") return printJson(await client.listPosts(projectId, { limit: flags.limit }));
171
+ if (subcommand === "get") return printJson(await client.getPost(projectId, required(rest[0], "post id")));
172
+ if (subcommand === "create") {
173
+ const title = flags.title || rest.join(" ").trim();
174
+ if (!title) throw new Error("Usage: genseo posts create --title \"Title\"");
175
+ return printJson(await client.createPost(projectId, {
176
+ title,
177
+ keyword_id: flags["keyword-id"] || flags.keywordId,
178
+ scheduled_date: flags["scheduled-date"] || flags.scheduledDate,
179
+ status: flags.status,
180
+ draft_only: flags["draft-only"] !== "false",
181
+ content: flags["content-json"] ? JSON.parse(flags["content-json"]) : undefined,
182
+ }));
183
+ }
184
+ if (subcommand === "update") {
185
+ const postId = required(rest[0], "post id");
186
+ const body = compact({
187
+ title: flags.title,
188
+ status: flags.status,
189
+ scheduled_date: flags["scheduled-date"] || flags.scheduledDate,
190
+ meta_description: flags["meta-description"] || flags.metaDescription,
191
+ content: flags["content-json"] ? JSON.parse(flags["content-json"]) : undefined,
192
+ });
193
+ return printJson(await client.updatePost(projectId, postId, body));
194
+ }
195
+ if (subcommand === "generate") return printJson(await client.generatePost(projectId, required(rest[0], "post id")));
196
+ if (subcommand === "publish") return printJson(await client.publishPost(projectId, required(rest[0], "post id")));
197
+ throw new Error(`Unknown posts command: ${subcommand}`);
198
+ }
199
+
200
+ async function integrations(subcommand, rest, flags) {
201
+ const client = await createClient(flagsToConfig(flags));
202
+ const projectId = await resolveProjectId(client, flags);
203
+ if (subcommand === "list") return printJson(await client.listIntegrations(projectId));
204
+ if (subcommand === "fields") return printJson(await client.integrationFields(projectId, required(rest[0] || flags.provider, "provider")));
205
+ if (subcommand === "connect-url") return printJson(await client.integrationConnectUrl(projectId, required(rest[0] || flags.provider, "provider")));
206
+ if (subcommand === "mapping") {
207
+ const provider = required(rest[0] || flags.provider, "provider");
208
+ return printJson(await client.saveIntegrationMapping(projectId, provider, {
209
+ connection_id: flags["connection-id"] || flags.connectionId,
210
+ site_id: flags["site-id"] || flags.siteId,
211
+ collection_id: flags["collection-id"] || flags.collectionId,
212
+ blog_id: flags["blog-id"] || flags.blogId,
213
+ field_map: flags["field-map-json"] ? JSON.parse(flags["field-map-json"]) : undefined,
214
+ }));
215
+ }
216
+ throw new Error(`Unknown integrations command: ${subcommand}`);
217
+ }
218
+
219
+ async function webhooks(subcommand, rest, flags) {
220
+ const client = await createClient(flagsToConfig(flags));
221
+ const projectId = await resolveProjectId(client, flags);
222
+ if (subcommand === "list") return printJson(await client.listWebhooks(projectId));
223
+ if (subcommand === "create") {
224
+ const targetUrl = flags["target-url"] || flags.targetUrl || rest[0];
225
+ if (!targetUrl) throw new Error("Usage: genseo webhooks create --target-url https://example.com/hook");
226
+ return printJson(await client.createWebhook(projectId, {
227
+ target_url: targetUrl,
228
+ event: flags.event,
229
+ secret: flags.secret,
230
+ }));
231
+ }
232
+ if (subcommand === "delete") return printJson(await client.deleteWebhook(projectId, required(rest[0], "webhook id")));
233
+ throw new Error(`Unknown webhooks command: ${subcommand}`);
234
+ }
235
+
236
+ async function resolveProjectId(client, flags) {
237
+ const projectId = flags["project-id"] || flags.projectId;
238
+ if (projectId) return projectId;
239
+ const me = await client.me();
240
+ return me.project.id;
241
+ }
242
+
243
+ function parseArgs(argv) {
244
+ const flags = {};
245
+ const positionals = [];
246
+ for (let i = 0; i < argv.length; i += 1) {
247
+ const arg = argv[i];
248
+ if (arg === "--") {
249
+ positionals.push(...argv.slice(i + 1));
250
+ break;
251
+ }
252
+ if (arg.startsWith("--")) {
253
+ const [rawKey, rawValue] = arg.slice(2).split("=", 2);
254
+ const key = rawKey;
255
+ if (rawValue !== undefined) {
256
+ flags[key] = rawValue;
257
+ } else if (argv[i + 1] && !argv[i + 1].startsWith("--")) {
258
+ flags[key] = argv[i + 1];
259
+ i += 1;
260
+ } else {
261
+ flags[key] = true;
262
+ }
263
+ continue;
264
+ }
265
+ positionals.push(arg);
266
+ }
267
+ return { flags, positionals };
268
+ }
269
+
270
+ function flagsToConfig(flags) {
271
+ return {
272
+ apiBase: flags["api-base"] || flags.apiBase,
273
+ authBase: flags["auth-base"] || flags.authBase,
274
+ apiKey: flags["api-key"] || flags.apiKey,
275
+ };
276
+ }
277
+
278
+ function printJson(data) {
279
+ console.log(JSON.stringify(data, null, 2));
280
+ }
281
+
282
+ function required(value, name) {
283
+ if (!value) throw new Error(`Missing ${name}.`);
284
+ return value;
285
+ }
286
+
287
+ function compact(obj) {
288
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
289
+ }
290
+
291
+ function numberOrNull(value) {
292
+ if (value === undefined || value === null || value === "") return undefined;
293
+ const parsed = Number(value);
294
+ return Number.isFinite(parsed) ? parsed : undefined;
295
+ }
296
+
297
+ function booleanFlag(value) {
298
+ if (value === undefined || value === null || value === "") return undefined;
299
+ if (value === true) return true;
300
+ if (value === false) return false;
301
+ return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
302
+ }
303
+
304
+ function jsonFlag(value) {
305
+ if (value === undefined || value === null || value === "") return undefined;
306
+ return JSON.parse(String(value));
307
+ }
308
+
309
+ function stringList(value) {
310
+ if (!value) return [];
311
+ if (Array.isArray(value)) return value;
312
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
313
+ }
314
+
315
+ function openBrowser(url) {
316
+ const platform = process.platform;
317
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
318
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
319
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
320
+ child.unref();
321
+ }
322
+
323
+ function printHelp() {
324
+ console.log(`Genseo CLI
325
+
326
+ Usage:
327
+ genseo login [--api-base https://api.genseo.co/v1] [--no-open]
328
+ genseo login --api-key gs_live_...
329
+ genseo logout
330
+ genseo me
331
+ genseo project get
332
+ genseo project create --domain example.com --name "Example" [--generate-keywords] [--issue-api-key]
333
+ genseo keywords list [--limit 50]
334
+ genseo keywords add "keyword" [--volume 100] [--kd 20]
335
+ genseo keywords generate
336
+ genseo keywords metrics "keyword one" "keyword two"
337
+ genseo posts list [--limit 50]
338
+ genseo posts create --title "Post title" [--keyword-id id]
339
+ genseo posts get <postId>
340
+ genseo posts update <postId> [--title "New title"] [--status draft]
341
+ genseo posts generate <postId>
342
+ genseo posts publish <postId>
343
+ genseo integrations list
344
+ genseo integrations fields <provider>
345
+ genseo integrations connect-url <provider>
346
+ genseo integrations mapping <provider> --connection-id id --field-map-json '{"title":"name"}'
347
+ genseo webhooks list
348
+ genseo webhooks create --target-url https://example.com/hook [--event post.published]
349
+ genseo webhooks delete <webhookId>
350
+
351
+ Auth:
352
+ Prefer \`genseo login\`, or set GENSEO_API_KEY and GENSEO_API_BASE.
353
+ `);
354
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export const DEFAULT_API_BASE = "https://api.genseo.co/v1";
6
+ export const DEFAULT_AUTH_BASE = "https://api.genseo.co";
7
+
8
+ function configPath() {
9
+ return process.env.GENSEO_CONFIG_FILE || join(homedir(), ".genseo", "config.json");
10
+ }
11
+
12
+ export async function readStoredConfig() {
13
+ try {
14
+ return JSON.parse(await readFile(configPath(), "utf8"));
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ export async function writeStoredConfig(config) {
21
+ const file = configPath();
22
+ await mkdir(dirname(file), { recursive: true });
23
+ await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
24
+ }
25
+
26
+ export async function clearStoredConfig() {
27
+ await rm(configPath(), { force: true });
28
+ }
29
+
30
+ export async function resolveConfig(overrides = {}) {
31
+ const stored = await readStoredConfig();
32
+ const apiBase =
33
+ overrides.apiBase ||
34
+ process.env.GENSEO_API_BASE ||
35
+ stored.apiBase ||
36
+ DEFAULT_API_BASE;
37
+ const apiKey =
38
+ overrides.apiKey ||
39
+ process.env.GENSEO_API_KEY ||
40
+ stored.apiKey ||
41
+ "";
42
+
43
+ const normalizedApiBase = String(apiBase).replace(/\/$/, "");
44
+ const authBase =
45
+ overrides.authBase ||
46
+ process.env.GENSEO_AUTH_BASE ||
47
+ stored.authBase ||
48
+ normalizedApiBase.replace(/\/v1$/, "");
49
+
50
+ return {
51
+ apiBase: normalizedApiBase,
52
+ authBase: String(authBase).replace(/\/$/, ""),
53
+ apiKey,
54
+ };
55
+ }
56
+
57
+ export async function saveLogin({ apiBase, authBase, apiKey }) {
58
+ const current = await readStoredConfig();
59
+ await writeStoredConfig({
60
+ ...current,
61
+ apiBase: apiBase.replace(/\/$/, ""),
62
+ authBase: authBase.replace(/\/$/, ""),
63
+ apiKey,
64
+ });
65
+ }
@@ -0,0 +1,116 @@
1
+ import { createClient } from "./api-client.mjs";
2
+ import { callTool, tools } from "./mcp-tools.mjs";
3
+
4
+ export async function runMcpServer() {
5
+ const transport = new StdioJsonRpcTransport(process.stdin, process.stdout);
6
+ const client = await createClient();
7
+
8
+ transport.onMessage(async (message) => {
9
+ try {
10
+ if (message.method === "initialize") {
11
+ return transport.respond(message.id, {
12
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
13
+ capabilities: { tools: {} },
14
+ serverInfo: { name: "genseo-mcp", version: "0.1.0" },
15
+ });
16
+ }
17
+
18
+ if (message.method === "notifications/initialized") {
19
+ return;
20
+ }
21
+
22
+ if (message.method === "tools/list") {
23
+ return transport.respond(message.id, { tools });
24
+ }
25
+
26
+ if (message.method === "ping") {
27
+ return transport.respond(message.id, {});
28
+ }
29
+
30
+ if (message.method === "resources/list") {
31
+ return transport.respond(message.id, { resources: [] });
32
+ }
33
+
34
+ if (message.method === "prompts/list") {
35
+ return transport.respond(message.id, { prompts: [] });
36
+ }
37
+
38
+ if (message.method === "tools/call") {
39
+ const name = message.params?.name;
40
+ const args = message.params?.arguments || {};
41
+ const result = await callTool(client, name, args);
42
+ return transport.respond(message.id, {
43
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
44
+ });
45
+ }
46
+
47
+ return transport.error(message.id, -32601, `Method not found: ${message.method}`);
48
+ } catch (error) {
49
+ return transport.respond(message.id, {
50
+ isError: true,
51
+ content: [{
52
+ type: "text",
53
+ text: error instanceof Error ? error.message : String(error),
54
+ }],
55
+ });
56
+ }
57
+ });
58
+
59
+ transport.start();
60
+ }
61
+
62
+ class StdioJsonRpcTransport {
63
+ constructor(input, output) {
64
+ this.input = input;
65
+ this.output = output;
66
+ this.buffer = Buffer.alloc(0);
67
+ this.handlers = [];
68
+ }
69
+
70
+ onMessage(handler) {
71
+ this.handlers.push(handler);
72
+ }
73
+
74
+ start() {
75
+ this.input.on("data", (chunk) => {
76
+ this.buffer = Buffer.concat([this.buffer, chunk]);
77
+ this.readMessages();
78
+ });
79
+ }
80
+
81
+ readMessages() {
82
+ while (true) {
83
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
84
+ if (headerEnd === -1) return;
85
+ const header = this.buffer.slice(0, headerEnd).toString("utf8");
86
+ const match = header.match(/Content-Length:\s*(\d+)/i);
87
+ if (!match) {
88
+ this.buffer = this.buffer.slice(headerEnd + 4);
89
+ continue;
90
+ }
91
+ const length = Number(match[1]);
92
+ const bodyStart = headerEnd + 4;
93
+ const bodyEnd = bodyStart + length;
94
+ if (this.buffer.length < bodyEnd) return;
95
+ const body = this.buffer.slice(bodyStart, bodyEnd).toString("utf8");
96
+ this.buffer = this.buffer.slice(bodyEnd);
97
+ const message = JSON.parse(body);
98
+ for (const handler of this.handlers) void handler(message);
99
+ }
100
+ }
101
+
102
+ respond(id, result) {
103
+ if (id === undefined || id === null) return;
104
+ this.send({ jsonrpc: "2.0", id, result });
105
+ }
106
+
107
+ error(id, code, message) {
108
+ if (id === undefined || id === null) return;
109
+ this.send({ jsonrpc: "2.0", id, error: { code, message } });
110
+ }
111
+
112
+ send(message) {
113
+ const body = JSON.stringify(message);
114
+ this.output.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
115
+ }
116
+ }
@@ -0,0 +1,257 @@
1
+ export const tools = [
2
+ {
3
+ name: "genseo_me",
4
+ description: "Return API key metadata and the bound Genseo project. Call this first.",
5
+ inputSchema: objectSchema({}),
6
+ },
7
+ {
8
+ name: "genseo_project_get",
9
+ description: "Read the bound Genseo project.",
10
+ inputSchema: objectSchema({ project_id: stringProp("Optional explicit project id. Must match the bound project.") }),
11
+ },
12
+ {
13
+ name: "genseo_project_create",
14
+ description: "Create a new Genseo project in the same workspace. Requires projects:write and an existing active paid project in the workspace.",
15
+ inputSchema: objectSchema({
16
+ domain: stringProp("Project domain, e.g. example.com."),
17
+ name: stringProp("Optional project name."),
18
+ description: stringProp("Optional business description."),
19
+ category: stringProp("Optional category."),
20
+ business_type: stringProp("Optional business type."),
21
+ region: stringProp("Optional target region."),
22
+ language_code: stringProp("Optional language code."),
23
+ sitemap_url: stringProp("Optional sitemap URL."),
24
+ blog_url: stringProp("Optional blog URL."),
25
+ business_goal: stringProp("Optional business goal."),
26
+ faqs: { type: "array", items: { type: "object", additionalProperties: true }, description: "Optional FAQ objects." },
27
+ competitors: { type: "array", items: { type: "object", additionalProperties: true }, description: "Optional competitor objects." },
28
+ ctas: { type: "array", items: { type: "object", additionalProperties: true }, description: "Optional CTA objects." },
29
+ generate_keywords: { type: "boolean", description: "Start keyword generation after creation." },
30
+ issue_api_key: { type: "boolean", description: "Issue a new one-time API key for the new project." },
31
+ api_key_name: stringProp("Name for the new API key."),
32
+ api_key_scopes: { type: "array", items: { type: "string" }, description: "Scopes for the new API key." },
33
+ }, ["domain"]),
34
+ },
35
+ {
36
+ name: "genseo_keywords_list",
37
+ description: "List keywords for the bound project.",
38
+ inputSchema: objectSchema({ project_id: projectIdProp(), limit: numberProp("Maximum number of keywords.") }),
39
+ },
40
+ {
41
+ name: "genseo_keywords_create",
42
+ description: "Add one custom keyword to the bound project.",
43
+ inputSchema: objectSchema({
44
+ project_id: projectIdProp(),
45
+ keyword: stringProp("Keyword text."),
46
+ volume: numberProp("Optional search volume."),
47
+ kd: numberProp("Optional keyword difficulty 1-100."),
48
+ intent: stringProp("Optional intent."),
49
+ }, ["keyword"]),
50
+ },
51
+ {
52
+ name: "genseo_keywords_generate",
53
+ description: "Start keyword generation for the bound project.",
54
+ inputSchema: objectSchema({ project_id: projectIdProp() }),
55
+ },
56
+ {
57
+ name: "genseo_keyword_metrics",
58
+ description: "Read stored keyword metrics for given keywords.",
59
+ inputSchema: objectSchema({
60
+ project_id: projectIdProp(),
61
+ keywords: { type: "array", items: { type: "string" }, description: "Keywords to look up." },
62
+ }, ["keywords"]),
63
+ },
64
+ {
65
+ name: "genseo_posts_list",
66
+ description: "List posts in the bound project.",
67
+ inputSchema: objectSchema({ project_id: projectIdProp(), limit: numberProp("Maximum number of posts.") }),
68
+ },
69
+ {
70
+ name: "genseo_posts_create_draft",
71
+ description: "Create a draft post. Prefer this before generation or publishing.",
72
+ inputSchema: objectSchema({
73
+ project_id: projectIdProp(),
74
+ title: stringProp("Draft post title."),
75
+ keyword_id: stringProp("Optional keyword id."),
76
+ scheduled_date: stringProp("Optional scheduled date."),
77
+ content: { description: "Optional EditorJS/Tiptap-compatible content JSON." },
78
+ }, ["title"]),
79
+ },
80
+ {
81
+ name: "genseo_posts_get",
82
+ description: "Read a single post from the bound project.",
83
+ inputSchema: objectSchema({ project_id: projectIdProp(), post_id: stringProp("Post id.") }, ["post_id"]),
84
+ },
85
+ {
86
+ name: "genseo_posts_update",
87
+ description: "Patch a post. Preserve existing user content unless intentionally changing it.",
88
+ inputSchema: objectSchema({
89
+ project_id: projectIdProp(),
90
+ post_id: stringProp("Post id."),
91
+ title: stringProp("Optional title."),
92
+ status: stringProp("Optional status."),
93
+ scheduled_date: stringProp("Optional scheduled date."),
94
+ meta_description: stringProp("Optional meta description."),
95
+ content: { description: "Optional content JSON." },
96
+ }, ["post_id"]),
97
+ },
98
+ {
99
+ name: "genseo_posts_generate",
100
+ description: "Start async AI generation for a post.",
101
+ inputSchema: objectSchema({ project_id: projectIdProp(), post_id: stringProp("Post id.") }, ["post_id"]),
102
+ },
103
+ {
104
+ name: "genseo_posts_publish",
105
+ description: "Publish a post through configured integrations. Ask user before calling unless autonomous publishing is explicitly enabled.",
106
+ inputSchema: objectSchema({ project_id: projectIdProp(), post_id: stringProp("Post id.") }, ["post_id"]),
107
+ },
108
+ {
109
+ name: "genseo_integrations_list",
110
+ description: "List integration readiness for the bound project.",
111
+ inputSchema: objectSchema({ project_id: projectIdProp() }),
112
+ },
113
+ {
114
+ name: "genseo_integration_fields",
115
+ description: "List mappable fields for a provider.",
116
+ inputSchema: objectSchema({ project_id: projectIdProp(), provider: providerProp() }, ["provider"]),
117
+ },
118
+ {
119
+ name: "genseo_integration_connect_url",
120
+ description: "Create a browser URL for connecting a publishing provider.",
121
+ inputSchema: objectSchema({ project_id: projectIdProp(), provider: providerProp() }, ["provider"]),
122
+ },
123
+ {
124
+ name: "genseo_integration_mapping_save",
125
+ description: "Save a supported integration field mapping.",
126
+ inputSchema: objectSchema({
127
+ project_id: projectIdProp(),
128
+ provider: providerProp(),
129
+ connection_id: stringProp("Connection id."),
130
+ site_id: stringProp("Provider site id."),
131
+ collection_id: stringProp("Provider collection id."),
132
+ blog_id: stringProp("Provider blog id."),
133
+ field_map: { type: "object", additionalProperties: true, description: "Provider field map." },
134
+ }, ["provider", "connection_id"]),
135
+ },
136
+ {
137
+ name: "genseo_webhooks_list",
138
+ description: "List webhooks for the bound project.",
139
+ inputSchema: objectSchema({ project_id: projectIdProp() }),
140
+ },
141
+ {
142
+ name: "genseo_webhooks_create",
143
+ description: "Create a webhook for the bound project.",
144
+ inputSchema: objectSchema({
145
+ project_id: projectIdProp(),
146
+ target_url: stringProp("Webhook URL."),
147
+ event: stringProp("Event name, defaults to post.published."),
148
+ secret: stringProp("Optional webhook secret."),
149
+ }, ["target_url"]),
150
+ },
151
+ {
152
+ name: "genseo_webhooks_delete",
153
+ description: "Delete a webhook from the bound project.",
154
+ inputSchema: objectSchema({ project_id: projectIdProp(), webhook_id: stringProp("Webhook id.") }, ["webhook_id"]),
155
+ },
156
+ ];
157
+
158
+ export async function callTool(client, name, args = {}) {
159
+ switch (name) {
160
+ case "genseo_me":
161
+ return client.me();
162
+ case "genseo_project_get":
163
+ return client.project(args.project_id);
164
+ case "genseo_project_create":
165
+ return client.createProject(pick(args, [
166
+ "domain",
167
+ "name",
168
+ "description",
169
+ "category",
170
+ "business_type",
171
+ "region",
172
+ "language_code",
173
+ "sitemap_url",
174
+ "blog_url",
175
+ "business_goal",
176
+ "faqs",
177
+ "competitors",
178
+ "ctas",
179
+ "generate_keywords",
180
+ "issue_api_key",
181
+ "api_key_name",
182
+ "api_key_scopes",
183
+ ]));
184
+ case "genseo_keywords_list":
185
+ return client.listKeywords(await resolveProjectId(client, args), { limit: args.limit });
186
+ case "genseo_keywords_create":
187
+ return client.createKeyword(await resolveProjectId(client, args), pick(args, ["keyword", "volume", "kd", "intent"]));
188
+ case "genseo_keywords_generate":
189
+ return client.generateKeywords(await resolveProjectId(client, args));
190
+ case "genseo_keyword_metrics":
191
+ return client.keywordMetrics(await resolveProjectId(client, args), args.keywords || []);
192
+ case "genseo_posts_list":
193
+ return client.listPosts(await resolveProjectId(client, args), { limit: args.limit });
194
+ case "genseo_posts_create_draft":
195
+ return client.createPost(await resolveProjectId(client, args), {
196
+ title: args.title,
197
+ keyword_id: args.keyword_id,
198
+ scheduled_date: args.scheduled_date,
199
+ content: args.content,
200
+ draft_only: true,
201
+ });
202
+ case "genseo_posts_get":
203
+ return client.getPost(await resolveProjectId(client, args), args.post_id);
204
+ case "genseo_posts_update":
205
+ return client.updatePost(await resolveProjectId(client, args), args.post_id, pick(args, ["title", "status", "scheduled_date", "meta_description", "content"]));
206
+ case "genseo_posts_generate":
207
+ return client.generatePost(await resolveProjectId(client, args), args.post_id);
208
+ case "genseo_posts_publish":
209
+ return client.publishPost(await resolveProjectId(client, args), args.post_id);
210
+ case "genseo_integrations_list":
211
+ return client.listIntegrations(await resolveProjectId(client, args));
212
+ case "genseo_integration_fields":
213
+ return client.integrationFields(await resolveProjectId(client, args), args.provider);
214
+ case "genseo_integration_connect_url":
215
+ return client.integrationConnectUrl(await resolveProjectId(client, args), args.provider);
216
+ case "genseo_integration_mapping_save":
217
+ return client.saveIntegrationMapping(await resolveProjectId(client, args), args.provider, pick(args, ["connection_id", "site_id", "collection_id", "blog_id", "field_map"]));
218
+ case "genseo_webhooks_list":
219
+ return client.listWebhooks(await resolveProjectId(client, args));
220
+ case "genseo_webhooks_create":
221
+ return client.createWebhook(await resolveProjectId(client, args), pick(args, ["target_url", "event", "secret"]));
222
+ case "genseo_webhooks_delete":
223
+ return client.deleteWebhook(await resolveProjectId(client, args), args.webhook_id);
224
+ default:
225
+ throw new Error(`Unknown tool: ${name}`);
226
+ }
227
+ }
228
+
229
+ async function resolveProjectId(client, args) {
230
+ if (args.project_id) return args.project_id;
231
+ const me = await client.me();
232
+ return me.project.id;
233
+ }
234
+
235
+ function pick(obj, keys) {
236
+ return Object.fromEntries(keys.filter((key) => obj[key] !== undefined).map((key) => [key, obj[key]]));
237
+ }
238
+
239
+ function objectSchema(properties, required = []) {
240
+ return { type: "object", properties, required, additionalProperties: false };
241
+ }
242
+
243
+ function stringProp(description) {
244
+ return { type: "string", description };
245
+ }
246
+
247
+ function numberProp(description) {
248
+ return { type: "number", description };
249
+ }
250
+
251
+ function projectIdProp() {
252
+ return stringProp("Optional project id. If omitted, the server calls /me and uses the bound project.");
253
+ }
254
+
255
+ function providerProp() {
256
+ return { type: "string", enum: ["webflow", "shopify", "wordpress", "framer", "wix"], description: "Integration provider." };
257
+ }