-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathstub-server.mjs
More file actions
201 lines (184 loc) · 8.26 KB
/
stub-server.mjs
File metadata and controls
201 lines (184 loc) · 8.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env node
// Minimal stdio MCP server stub for Glama Docker verification.
// Exposes the public tool shape used for registry scoring.
// For live usage, connect to: https://xquik.com/mcp
import { createInterface } from "node:readline";
const SERVER_INFO = {
name: "xquik",
version: "2.4.12",
};
const CAPABILITIES = {
tools: { listChanged: false },
};
const MAX_LINE_LENGTH = 64 * 1024;
const TOOLS = [
{
name: "explore",
description:
"Search and browse the Xquik X (Twitter) API specification to discover endpoints before making live API calls with the 'xquik' tool.\n\n" +
"## When to use\n" +
"- Use 'explore' FIRST to find the right endpoint path, parameters, and response shape before calling 'xquik'.\n" +
"- Use when the user asks what capabilities are available or how to accomplish a task on X/Twitter.\n" +
"- Use to check whether an endpoint is free or requires a subscription.\n\n" +
"## When NOT to use\n" +
"- Do NOT use 'explore' to fetch live data from X - use 'xquik' instead.\n" +
"- Do NOT use if you already know the endpoint path and parameters.\n\n" +
"## Behavior\n" +
"- Read-only, idempotent. No network calls - runs against an in-memory catalog of 100+ endpoints.\n" +
"- Always free, no authentication or credits required.\n" +
"- Returns the result of your filter function (e.g., empty array if no endpoints match).\n" +
"- Returns a validation error if the request function is invalid.\n" +
"- Timeout: 60 seconds.\n" +
"- Each EndpointInfo contains: method, path, summary, category (account | composition | credits | extraction | media | monitoring | support | twitter | x-accounts | x-write), free (boolean), parameters (array), and responseShape (string).\n\n" +
"## Input format\n" +
"Provide a bounded request function. The server exposes `spec.endpoints` (EndpointInfo[]). Filter, search, or return them.\n\n" +
"## Examples\n" +
"Find all free endpoints: `async () => spec.endpoints.filter(e => e.free)`\n" +
"Find by category: `async () => spec.endpoints.filter(e => e.category === 'composition')`\n" +
"Search by keyword: `async () => spec.endpoints.filter(e => e.summary.toLowerCase().includes('tweet'))`\n" +
"Get full details: `async () => spec.endpoints.find(e => e.path === '/api/v1/x/tweets/search')`",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
maxLength: 4096,
description:
"Bounded request function that filters or searches spec.endpoints (EndpointInfo[]). Must return an array or single EndpointInfo object. Example: async () => spec.endpoints.filter(e => e.category === 'twitter')",
},
},
required: ["code"],
},
annotations: {
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: true,
},
},
{
name: "xquik",
description:
"Send confirmed Xquik API requests across 100+ REST endpoints.\n\n" +
"## When to use\n" +
"- Use after calling 'explore' to discover the endpoint path and parameters.\n" +
"- Use for live X/Twitter operations such as tweet search, user lookup, giveaway draws, extraction jobs, composition, private reads, persistent monitors, webhooks, and confirmation-gated writes.\n" +
"- Confirm private reads, persistent resources, billing flows, and writes before using endpoints that require user approval.\n\n" +
"## When NOT to use\n" +
"- Do NOT use to discover endpoints - use 'explore' first.\n" +
"- Do NOT pass API keys or auth headers - authentication is injected automatically.\n\n" +
"## Behavior\n" +
"- Processes the provided request function with `xquik.request(path, options?)` and `spec.endpoints` available.\n" +
"- No filesystem or arbitrary network access - only xquik.request() is available. Console calls are silently ignored.\n" +
"- Timeout: 60 seconds per invocation, 60 seconds per individual API request.\n" +
"- Read operations (GET) return JSON objects with the requested data. Mutating operations (POST/PATCH/DELETE) require prior user confirmation and return `{ success: true }` or `{ success: true, warning: '...' }`.\n" +
"- Pagination: responses include `has_next_page` (boolean) and `next_cursor` (string). Pass `cursor` as a query param for the next page.\n" +
"- Some operations modify X or Xquik resources. Show the exact payload, target, and cost before calling them.\n\n" +
"## Error handling\n" +
"- 402: Subscription required or insufficient credits. Explain the billing issue and ask before any checkout or top-up action.\n" +
"- 429: Rate limited. Retry after backoff.\n" +
"- 404: Resource not found (user, tweet, or monitor does not exist).\n" +
"- 200 with `warning` field: Probable success - do NOT retry.\n\n" +
"## Costs\n" +
"- Free: compose, styles, drafts, radar, account info, support, credit balance, and webhook management.\n" +
"- 1 credit/read ($0.00015): tweet search, timeline, bookmarks, favoriters.\n" +
"- 10 credits/write ($0.0015): tweet, like, retweet, follow, DM.\n\n" +
"## Input format\n" +
"Provide a bounded request function using `xquik.request(path, { method?, body?, query? })`. Auth is automatic.\n\n" +
"## Examples\n" +
"Search tweets: `async () => xquik.request('/api/v1/x/tweets/search', { query: { q: 'AI agents', limit: '50' } })`\n" +
"Get user: `async () => xquik.request('/api/v1/x/users/elonmusk')`\n" +
"After explicit user confirmation, post tweet: `async () => xquik.request('/api/v1/x/tweets', { method: 'POST', body: { account: '<confirmed_account>', text: '<confirmed_text>' } })`",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
maxLength: 4096,
description:
"Bounded request function that calls xquik.request(path, options?) to perform X/Twitter API operations. Auth is injected automatically. Example: async () => xquik.request('/api/v1/x/tweets/search', { query: { q: 'AI', limit: '20' } })",
},
},
required: ["code"],
},
annotations: {
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
readOnlyHint: false,
},
},
];
const rl = createInterface({ input: process.stdin, terminal: false });
function send(msg) {
const json = JSON.stringify(msg);
process.stdout.write(json + "\n");
}
function handleMessage(msg) {
const { id, method, params } = msg;
switch (method) {
case "initialize":
return send({
jsonrpc: "2.0",
id,
result: {
protocolVersion: "2024-11-05",
serverInfo: SERVER_INFO,
capabilities: CAPABILITIES,
},
});
case "notifications/initialized":
return; // no response needed
case "tools/list":
return send({
jsonrpc: "2.0",
id,
result: { tools: TOOLS },
});
case "tools/call": {
const toolName = params?.name;
if (toolName === "explore" || toolName === "xquik") {
return send({
jsonrpc: "2.0",
id,
result: {
content: [
{
type: "text",
text: `This is a verification stub. Connect to the live server at https://xquik.com/mcp for real API access. Configure with: { "mcpServers": { "xquik": { "url": "https://xquik.com/mcp", "headers": { "x-api-key": "<YOUR_API_KEY>" } } } }`,
},
],
},
});
}
return send({
jsonrpc: "2.0",
id,
error: { code: -32601, message: `Unknown tool: ${toolName}` },
});
}
case "ping":
return send({ jsonrpc: "2.0", id, result: {} });
default:
if (id !== undefined) {
return send({
jsonrpc: "2.0",
id,
error: { code: -32601, message: `Method not found: ${method}` },
});
}
}
}
rl.on("line", (line) => {
if (line.length > MAX_LINE_LENGTH) {
return;
}
try {
const msg = JSON.parse(line);
if (msg !== null && typeof msg === "object" && !Array.isArray(msg)) {
handleMessage(msg);
}
} catch {
// ignore malformed input
}
});