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.
- package/README.md +6 -219
- package/bin/suparank.js +85 -536
- package/credentials.example.json +36 -18
- package/mcp-client/handlers/action.js +33 -0
- package/mcp-client/handlers/backend.js +43 -0
- package/mcp-client/handlers/index.js +9 -0
- package/mcp-client/handlers/orchestrator.js +850 -0
- package/mcp-client/index.js +22 -11
- package/mcp-client/publishers/ghost.js +105 -0
- package/mcp-client/publishers/image.js +306 -0
- package/mcp-client/publishers/index.js +20 -0
- package/mcp-client/publishers/webhook.js +76 -0
- package/mcp-client/publishers/wordpress.js +220 -0
- package/mcp-client/server.js +220 -0
- package/mcp-client/services/credentials.js +22 -0
- package/mcp-client/services/index.js +1 -0
- package/mcp-client/services/project.js +40 -0
- package/mcp-client/tools/definitions.js +679 -0
- package/mcp-client/tools/discovery.js +132 -0
- package/mcp-client/tools/index.js +22 -0
- package/mcp-client/utils/content.js +126 -0
- package/mcp-client/utils/formatting.js +71 -0
- package/mcp-client/utils/index.js +2 -0
- package/mcp-client/workflow/index.js +10 -0
- package/mcp-client/workflow/planner.js +513 -0
- package/package.json +7 -19
- package/mcp-client.js +0 -3693
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|