suparank 1.2.6 → 1.2.8

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,220 @@
1
+ /**
2
+ * Suparank MCP - WordPress Publisher
3
+ *
4
+ * Publish content to WordPress using REST API or Suparank Connector plugin
5
+ */
6
+
7
+ import { log, progress } from '../utils/logging.js'
8
+ import { fetchWithRetry, fetchWithTimeout } from '../services/api.js'
9
+ import { getCredentials } from '../services/credentials.js'
10
+ import { sessionState } from '../services/session-state.js'
11
+ import { markdownToHtml } from '../utils/formatting.js'
12
+
13
+ /**
14
+ * Fetch available categories from WordPress
15
+ * @returns {Promise<Array|null>} Categories array or null
16
+ */
17
+ export async function fetchWordPressCategories() {
18
+ const credentials = getCredentials()
19
+ const wpConfig = credentials?.wordpress
20
+
21
+ if (!wpConfig?.secret_key || !wpConfig?.site_url) {
22
+ return null
23
+ }
24
+
25
+ try {
26
+ log('Fetching WordPress categories...')
27
+
28
+ // Try new Suparank endpoint first, then fall back to legacy
29
+ const endpoints = [
30
+ { url: `${wpConfig.site_url}/wp-json/suparank/v1/categories`, header: 'X-Suparank-Key' },
31
+ { url: `${wpConfig.site_url}/wp-json/writer-mcp/v1/categories`, header: 'X-Writer-MCP-Key' }
32
+ ]
33
+
34
+ for (const endpoint of endpoints) {
35
+ try {
36
+ const response = await fetchWithTimeout(endpoint.url, {
37
+ method: 'GET',
38
+ headers: {
39
+ [endpoint.header]: wpConfig.secret_key
40
+ }
41
+ }, 10000) // 10s timeout
42
+
43
+ if (response.ok) {
44
+ const result = await response.json()
45
+ if (result.success && result.categories) {
46
+ log(`Found ${result.categories.length} WordPress categories`)
47
+ return result.categories
48
+ }
49
+ }
50
+ } catch (e) {
51
+ // Try next endpoint
52
+ }
53
+ }
54
+
55
+ log('Failed to fetch categories from any endpoint')
56
+ return null
57
+ } catch (error) {
58
+ log(`Error fetching categories: ${error.message}`)
59
+ return null
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Publish content to WordPress
65
+ * @param {object} args - Publish arguments
66
+ * @param {string} args.title - Post title
67
+ * @param {string} args.content - Post content (markdown)
68
+ * @param {string} [args.status='draft'] - Publication status
69
+ * @param {string[]} [args.categories=[]] - Category names
70
+ * @param {string[]} [args.tags=[]] - Tag names
71
+ * @param {string} [args.featured_image_url] - Featured image URL
72
+ * @returns {Promise<object>} MCP response
73
+ */
74
+ export async function executeWordPressPublish(args) {
75
+ const credentials = getCredentials()
76
+ const wpConfig = credentials.wordpress
77
+ const { title, content, status = 'draft', categories = [], tags = [], featured_image_url } = args
78
+
79
+ progress('Publish', `Publishing to WordPress: "${title}"`)
80
+ log(`Publishing to WordPress: ${title}`)
81
+
82
+ // Convert markdown to HTML for WordPress
83
+ const htmlContent = markdownToHtml(content)
84
+
85
+ // Method 1: Use Suparank Connector plugin (secret_key auth)
86
+ if (wpConfig.secret_key) {
87
+ return publishWithPlugin(wpConfig, {
88
+ title,
89
+ htmlContent,
90
+ status,
91
+ categories,
92
+ tags,
93
+ featured_image_url
94
+ })
95
+ }
96
+
97
+ // Method 2: Use standard REST API with application password
98
+ if (wpConfig.app_password && wpConfig.username) {
99
+ return publishWithRestApi(wpConfig, {
100
+ title,
101
+ htmlContent,
102
+ status,
103
+ categories,
104
+ tags
105
+ })
106
+ }
107
+
108
+ throw new Error('WordPress credentials not configured. Add either secret_key (with plugin) or username + app_password to ~/.suparank/credentials.json')
109
+ }
110
+
111
+ /**
112
+ * Publish using Suparank/Writer MCP Connector plugin
113
+ */
114
+ async function publishWithPlugin(wpConfig, { title, htmlContent, status, categories, tags, featured_image_url }) {
115
+ log('Using Suparank/Writer MCP Connector plugin')
116
+
117
+ // Try new Suparank endpoint first, then fall back to legacy
118
+ const endpoints = [
119
+ { url: `${wpConfig.site_url}/wp-json/suparank/v1/publish`, header: 'X-Suparank-Key' },
120
+ { url: `${wpConfig.site_url}/wp-json/writer-mcp/v1/publish`, header: 'X-Writer-MCP-Key' }
121
+ ]
122
+
123
+ const postBody = JSON.stringify({
124
+ title,
125
+ content: htmlContent,
126
+ status,
127
+ categories,
128
+ tags,
129
+ featured_image_url,
130
+ excerpt: sessionState.metaDescription || ''
131
+ })
132
+
133
+ let lastError = null
134
+ for (const endpoint of endpoints) {
135
+ try {
136
+ const response = await fetchWithRetry(endpoint.url, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ [endpoint.header]: wpConfig.secret_key
141
+ },
142
+ body: postBody
143
+ }, 2, 30000) // 2 retries, 30s timeout
144
+
145
+ if (response.ok) {
146
+ const result = await response.json()
147
+
148
+ if (result.success) {
149
+ return formatSuccessResponse(result.post, status)
150
+ }
151
+ }
152
+ lastError = await response.text()
153
+ } catch (e) {
154
+ lastError = e.message
155
+ }
156
+ }
157
+
158
+ throw new Error(`WordPress error: ${lastError}`)
159
+ }
160
+
161
+ /**
162
+ * Publish using standard WordPress REST API
163
+ */
164
+ async function publishWithRestApi(wpConfig, { title, htmlContent, status, categories, tags }) {
165
+ log('Using WordPress REST API with application password')
166
+
167
+ const auth = Buffer.from(`${wpConfig.username}:${wpConfig.app_password}`).toString('base64')
168
+ const postData = {
169
+ title,
170
+ content: htmlContent,
171
+ status,
172
+ categories: [],
173
+ tags: []
174
+ }
175
+
176
+ const response = await fetch(`${wpConfig.site_url}/wp-json/wp/v2/posts`, {
177
+ method: 'POST',
178
+ headers: {
179
+ 'Authorization': `Basic ${auth}`,
180
+ 'Content-Type': 'application/json'
181
+ },
182
+ body: JSON.stringify(postData)
183
+ })
184
+
185
+ if (!response.ok) {
186
+ const error = await response.text()
187
+ throw new Error(`WordPress error: ${error}`)
188
+ }
189
+
190
+ const post = await response.json()
191
+
192
+ return {
193
+ content: [{
194
+ type: 'text',
195
+ text: `Post published to WordPress!\n\n**Title:** ${post.title.rendered}\n**Status:** ${post.status}\n**URL:** ${post.link}\n**ID:** ${post.id}\n\n${status === 'draft' ? 'The post is saved as a draft. Edit and publish from WordPress dashboard.' : 'The post is now live!'}`
196
+ }]
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Format success response for plugin publishing
202
+ */
203
+ function formatSuccessResponse(post, status) {
204
+ const categoriesInfo = post.categories?.length
205
+ ? `\n**Categories:** ${post.categories.join(', ')}`
206
+ : ''
207
+ const tagsInfo = post.tags?.length
208
+ ? `\n**Tags:** ${post.tags.join(', ')}`
209
+ : ''
210
+ const imageInfo = post.featured_image
211
+ ? `\n**Featured Image:** Uploaded`
212
+ : ''
213
+
214
+ return {
215
+ content: [{
216
+ type: 'text',
217
+ text: `Post published to WordPress!\n\n**Title:** ${post.title}\n**Status:** ${post.status}\n**URL:** ${post.url}\n**Edit:** ${post.edit_url}\n**ID:** ${post.id}${categoriesInfo}${tagsInfo}${imageInfo}\n\n${status === 'draft' ? 'The post is saved as a draft. Edit and publish from WordPress dashboard.' : 'The post is now live!'}`
218
+ }]
219
+ }
220
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Suparank MCP - Server Entry Point
3
+ *
4
+ * MCP server setup with stdio transport.
5
+ * Handles tool listing and execution.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ InitializeRequestSchema
14
+ } from '@modelcontextprotocol/sdk/types.js'
15
+
16
+ import { log, progress } from './utils/logging.js'
17
+ import { projectSlug, apiUrl } from './config.js'
18
+
19
+ // Services
20
+ import {
21
+ loadCredentials,
22
+ hasCredential,
23
+ getCredentials,
24
+ getExternalMCPs,
25
+ getCompositionHints
26
+ } from './services/credentials.js'
27
+ import { restoreSession } from './services/session-state.js'
28
+ import { incrementStat } from './services/stats.js'
29
+ import { fetchProjectConfig } from './services/project.js'
30
+
31
+ // Tools
32
+ import {
33
+ ORCHESTRATOR_TOOLS,
34
+ ACTION_TOOLS,
35
+ getAvailableTools
36
+ } from './tools/index.js'
37
+
38
+ // Handlers
39
+ import {
40
+ callBackendTool,
41
+ executeActionTool,
42
+ executeOrchestratorTool
43
+ } from './handlers/index.js'
44
+
45
+ /**
46
+ * Main server entry point
47
+ */
48
+ export async function main() {
49
+ log(`Starting MCP client for project: ${projectSlug}`)
50
+ log(`API URL: ${apiUrl}`)
51
+
52
+ // Load local credentials
53
+ const credentials = loadCredentials()
54
+ if (credentials) {
55
+ const configured = []
56
+ if (hasCredential('wordpress')) configured.push('wordpress')
57
+ if (hasCredential('ghost')) configured.push('ghost')
58
+ if (hasCredential('image')) {
59
+ const creds = getCredentials()
60
+ configured.push(`image:${creds.image_provider}`)
61
+ }
62
+ if (hasCredential('webhooks')) configured.push('webhooks')
63
+
64
+ const externalMcps = getExternalMCPs()
65
+ if (externalMcps.length > 0) {
66
+ configured.push(`mcps:${externalMcps.map(m => m.name).join(',')}`)
67
+ }
68
+
69
+ if (configured.length > 0) {
70
+ log(`Configured integrations: ${configured.join(', ')}`)
71
+ }
72
+ }
73
+
74
+ // Restore session state from previous run
75
+ if (restoreSession()) {
76
+ progress('Session', 'Restored previous workflow state')
77
+ }
78
+
79
+ // Fetch project configuration
80
+ progress('Init', 'Connecting to platform...')
81
+ let project
82
+ try {
83
+ project = await fetchProjectConfig()
84
+ progress('Init', `Connected to project: ${project.name}`)
85
+ } catch (error) {
86
+ log('Failed to load project config. Exiting.')
87
+ process.exit(1)
88
+ }
89
+
90
+ // Create MCP server
91
+ const server = new Server(
92
+ {
93
+ name: 'suparank',
94
+ version: '1.0.0'
95
+ },
96
+ {
97
+ capabilities: {
98
+ tools: {}
99
+ }
100
+ }
101
+ )
102
+
103
+ // Handle initialization
104
+ server.setRequestHandler(InitializeRequestSchema, async (request) => {
105
+ log('Received initialize request')
106
+ return {
107
+ protocolVersion: '2024-11-05',
108
+ capabilities: {
109
+ tools: {}
110
+ },
111
+ serverInfo: {
112
+ name: 'suparank',
113
+ version: '1.0.0'
114
+ }
115
+ }
116
+ })
117
+
118
+ // Handle tools list
119
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
120
+ log('Received list tools request')
121
+ const tools = getAvailableTools()
122
+ log(`Returning ${tools.length} tools (${ACTION_TOOLS.length} action tools)`)
123
+ return { tools }
124
+ })
125
+
126
+ // Handle tool calls
127
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
128
+ const { name, arguments: args } = request.params
129
+ progress('Tool', `Executing ${name}`)
130
+ log(`Executing tool: ${name}`)
131
+
132
+ // Track tool call stats
133
+ incrementStat('tool_calls')
134
+
135
+ // Check if this is an orchestrator tool
136
+ const orchestratorTool = ORCHESTRATOR_TOOLS.find(t => t.name === name)
137
+
138
+ if (orchestratorTool) {
139
+ try {
140
+ const result = await executeOrchestratorTool(name, args || {}, project)
141
+ log(`Orchestrator tool ${name} completed successfully`)
142
+ return result
143
+ } catch (error) {
144
+ log(`Orchestrator tool ${name} failed:`, error.message)
145
+ return {
146
+ content: [{
147
+ type: 'text',
148
+ text: `Error executing ${name}: ${error.message}`
149
+ }]
150
+ }
151
+ }
152
+ }
153
+
154
+ // Check if this is an action tool
155
+ const actionTool = ACTION_TOOLS.find(t => t.name === name)
156
+
157
+ if (actionTool) {
158
+ // Check credentials
159
+ if (!hasCredential(actionTool.requiresCredential)) {
160
+ return {
161
+ content: [{
162
+ type: 'text',
163
+ text: `Error: ${name} requires ${actionTool.requiresCredential} credentials.\n\nTo enable this tool:\n1. Run: npx suparank setup\n2. Add your ${actionTool.requiresCredential} credentials to ~/.suparank/credentials.json\n3. Restart the MCP server\n\nSee dashboard Settings > Credentials for setup instructions.`
164
+ }]
165
+ }
166
+ }
167
+
168
+ // Execute action tool locally
169
+ try {
170
+ const result = await executeActionTool(name, args || {})
171
+ log(`Action tool ${name} completed successfully`)
172
+ return result
173
+ } catch (error) {
174
+ log(`Action tool ${name} failed:`, error.message)
175
+ return {
176
+ content: [{
177
+ type: 'text',
178
+ text: `Error executing ${name}: ${error.message}`
179
+ }]
180
+ }
181
+ }
182
+ }
183
+
184
+ // Regular tool - call backend
185
+ try {
186
+ // Add composition hints if configured
187
+ const hints = getCompositionHints(name)
188
+ const externalMcps = getExternalMCPs()
189
+
190
+ const result = await callBackendTool(name, args || {})
191
+
192
+ // Inject composition hints into response if available
193
+ if (hints && result.content && result.content[0]?.text) {
194
+ const mcpList = externalMcps.length > 0
195
+ ? `\n\n## External MCPs Available\n${externalMcps.map(m => `- **${m.name}**: ${m.available_tools.join(', ')}`).join('\n')}`
196
+ : ''
197
+
198
+ result.content[0].text = result.content[0].text +
199
+ `\n\n---\n## Integration Hints\n${hints}${mcpList}`
200
+ }
201
+
202
+ log(`Tool ${name} completed successfully`)
203
+ return result
204
+ } catch (error) {
205
+ log(`Tool ${name} failed:`, error.message)
206
+ throw error
207
+ }
208
+ })
209
+
210
+ // Error handler
211
+ server.onerror = (error) => {
212
+ log('Server error:', error)
213
+ }
214
+
215
+ // Connect to stdio transport
216
+ const transport = new StdioServerTransport()
217
+ await server.connect(transport)
218
+
219
+ log('MCP server ready and listening on stdio')
220
+ }
@@ -125,3 +125,25 @@ export function getProviderConfig(provider) {
125
125
  export function clearCredentialsCache() {
126
126
  localCredentials = null
127
127
  }
128
+
129
+ /**
130
+ * Get list of external MCPs configured
131
+ * @returns {Array} Array of external MCP configurations
132
+ */
133
+ export function getExternalMCPs() {
134
+ const creds = getCredentials()
135
+ return creds?.external_mcps || []
136
+ }
137
+
138
+ /**
139
+ * Get composition hints for a specific tool
140
+ * @param {string} toolName - Tool name to get hints for
141
+ * @returns {string|null} Composition hints or null
142
+ */
143
+ export function getCompositionHints(toolName) {
144
+ const creds = getCredentials()
145
+ if (!creds?.tool_instructions) return null
146
+
147
+ const instruction = creds.tool_instructions.find(t => t.tool_name === toolName)
148
+ return instruction?.composition_hints || null
149
+ }
@@ -8,3 +8,4 @@ export * from './api.js'
8
8
  export * from './credentials.js'
9
9
  export * from './session-state.js'
10
10
  export * from './stats.js'
11
+ export * from './project.js'
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Suparank MCP - Project Service
3
+ *
4
+ * Fetch and manage project configuration from the Suparank API
5
+ */
6
+
7
+ import { log } from '../utils/logging.js'
8
+ import { fetchWithRetry } from './api.js'
9
+ import { apiUrl, apiKey, projectSlug } from '../config.js'
10
+
11
+ /**
12
+ * Fetch project configuration from the Suparank API
13
+ * @returns {Promise<object>} Project object with config
14
+ */
15
+ export async function fetchProjectConfig() {
16
+ try {
17
+ const response = await fetchWithRetry(`${apiUrl}/projects/${projectSlug}`, {
18
+ headers: {
19
+ 'Authorization': `Bearer ${apiKey}`,
20
+ 'Content-Type': 'application/json'
21
+ }
22
+ }, 3, 15000) // 3 retries, 15s timeout
23
+
24
+ if (!response.ok) {
25
+ const error = await response.text()
26
+
27
+ if (response.status === 401) {
28
+ throw new Error(`Invalid or expired API key. Please create a new one in the dashboard.`)
29
+ }
30
+
31
+ throw new Error(`Failed to fetch project: ${error}`)
32
+ }
33
+
34
+ const data = await response.json()
35
+ return data.project
36
+ } catch (error) {
37
+ log('Error fetching project config:', error.message)
38
+ throw error
39
+ }
40
+ }