sopo-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.
- package/.dockerignore +10 -0
- package/.env +7 -0
- package/.ghaymah.json +20 -0
- package/Dockerfile +37 -0
- package/GUIDE.md +311 -0
- package/README.md +143 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/sopo.prompts.d.ts +8 -0
- package/dist/prompts/sopo.prompts.d.ts.map +1 -0
- package/dist/prompts/sopo.prompts.js +102 -0
- package/dist/prompts/sopo.prompts.js.map +1 -0
- package/dist/resources/platform.resources.d.ts +9 -0
- package/dist/resources/platform.resources.d.ts.map +1 -0
- package/dist/resources/platform.resources.js +143 -0
- package/dist/resources/platform.resources.js.map +1 -0
- package/dist/sopo-client.d.ts +51 -0
- package/dist/sopo-client.d.ts.map +1 -0
- package/dist/sopo-client.js +145 -0
- package/dist/sopo-client.js.map +1 -0
- package/dist/tools/aggregate-request.tools.d.ts +8 -0
- package/dist/tools/aggregate-request.tools.d.ts.map +1 -0
- package/dist/tools/aggregate-request.tools.js +57 -0
- package/dist/tools/aggregate-request.tools.js.map +1 -0
- package/dist/tools/auth.tools.d.ts +12 -0
- package/dist/tools/auth.tools.d.ts.map +1 -0
- package/dist/tools/auth.tools.js +152 -0
- package/dist/tools/auth.tools.js.map +1 -0
- package/dist/tools/collection.tools.d.ts +8 -0
- package/dist/tools/collection.tools.d.ts.map +1 -0
- package/dist/tools/collection.tools.js +54 -0
- package/dist/tools/collection.tools.js.map +1 -0
- package/dist/tools/gateway-plugin.tools.d.ts +8 -0
- package/dist/tools/gateway-plugin.tools.d.ts.map +1 -0
- package/dist/tools/gateway-plugin.tools.js +64 -0
- package/dist/tools/gateway-plugin.tools.js.map +1 -0
- package/dist/tools/gateway-route.tools.d.ts +8 -0
- package/dist/tools/gateway-route.tools.d.ts.map +1 -0
- package/dist/tools/gateway-route.tools.js +68 -0
- package/dist/tools/gateway-route.tools.js.map +1 -0
- package/dist/tools/gateway.tools.d.ts +8 -0
- package/dist/tools/gateway.tools.d.ts.map +1 -0
- package/dist/tools/gateway.tools.js +56 -0
- package/dist/tools/gateway.tools.js.map +1 -0
- package/dist/tools/observability.tools.d.ts +9 -0
- package/dist/tools/observability.tools.d.ts.map +1 -0
- package/dist/tools/observability.tools.js +54 -0
- package/dist/tools/observability.tools.js.map +1 -0
- package/dist/tools/service-target.tools.d.ts +8 -0
- package/dist/tools/service-target.tools.d.ts.map +1 -0
- package/dist/tools/service-target.tools.js +52 -0
- package/dist/tools/service-target.tools.js.map +1 -0
- package/dist/tools/service.tools.d.ts +8 -0
- package/dist/tools/service.tools.d.ts.map +1 -0
- package/dist/tools/service.tools.js +67 -0
- package/dist/tools/service.tools.js.map +1 -0
- package/dist/tools/user-profile.tools.d.ts +8 -0
- package/dist/tools/user-profile.tools.d.ts.map +1 -0
- package/dist/tools/user-profile.tools.js +46 -0
- package/dist/tools/user-profile.tools.js.map +1 -0
- package/package.json +33 -0
- package/src/index.ts +115 -0
- package/src/prompts/sopo.prompts.ts +128 -0
- package/src/resources/platform.resources.ts +163 -0
- package/src/sopo-client.ts +180 -0
- package/src/tools/aggregate-request.tools.ts +83 -0
- package/src/tools/auth.tools.ts +187 -0
- package/src/tools/collection.tools.ts +80 -0
- package/src/tools/gateway-plugin.tools.ts +90 -0
- package/src/tools/gateway-route.tools.ts +94 -0
- package/src/tools/gateway.tools.ts +82 -0
- package/src/tools/observability.tools.ts +87 -0
- package/src/tools/service-target.tools.ts +78 -0
- package/src/tools/service.tools.ts +93 -0
- package/src/tools/user-profile.tools.ts +72 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sopo MCP Server — Resources
|
|
3
|
+
*
|
|
4
|
+
* MCP Resources provide read-only context to AI models about the Sopo platform.
|
|
5
|
+
* These are data sources the AI can read at any time to understand the system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sopoGet } from '../sopo-client.js';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
export function registerResources(server: McpServer): void {
|
|
12
|
+
|
|
13
|
+
// ── Platform Overview Resource ────────────────────────────────
|
|
14
|
+
server.resource(
|
|
15
|
+
'sopo-platform-overview',
|
|
16
|
+
'sopo://platform/overview',
|
|
17
|
+
{
|
|
18
|
+
description: 'Overview of the Sopo API Gateway platform, its architecture, domain model, and available API endpoints.',
|
|
19
|
+
mimeType: 'text/markdown',
|
|
20
|
+
},
|
|
21
|
+
async () => ({
|
|
22
|
+
contents: [{
|
|
23
|
+
uri: 'sopo://platform/overview',
|
|
24
|
+
mimeType: 'text/markdown',
|
|
25
|
+
text: `# Sopo API Gateway Platform
|
|
26
|
+
|
|
27
|
+
## What is Sopo?
|
|
28
|
+
Sopo is a full-featured **API Gateway management platform**. It allows users to create and manage API gateways that route, load-balance, and apply middleware to incoming HTTP requests.
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
- **Backend** (Node.js/TypeScript + Express): The BFF (Backend-for-Frontend) layer that exposes REST APIs and proxies GraphQL to Hasura.
|
|
32
|
+
- **Hasura**: GraphQL engine connected to PostgreSQL (metadata) and ClickHouse (logs/metrics).
|
|
33
|
+
- **Go Gateway Server**: The actual reverse proxy that processes live traffic based on the configuration defined via the Backend.
|
|
34
|
+
- **Frontend** (Next.js): Dashboard UI for managing gateways.
|
|
35
|
+
|
|
36
|
+
## Domain Model (Hierarchy)
|
|
37
|
+
\`\`\`
|
|
38
|
+
User
|
|
39
|
+
└── Gateway (API Gateway instance)
|
|
40
|
+
├── Service (upstream backend)
|
|
41
|
+
│ └── ServiceTarget (URL + weight for load balancing)
|
|
42
|
+
├── GatewayRoute (path → service mapping)
|
|
43
|
+
│ └── AggregateRequest (parallel sub-requests)
|
|
44
|
+
├── GatewayPlugin (middleware: auth, rate-limit, cors, etc.)
|
|
45
|
+
└── Collection (organizational group, Pro mode only)
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
## Key Concepts
|
|
49
|
+
- **Gateway**: A logical API gateway that users create. Has modes: "single" (simple) and "pro" (collections).
|
|
50
|
+
- **Service**: Represents an upstream backend with health checking and load balancing configuration.
|
|
51
|
+
- **ServiceTarget**: An actual URL endpoint for a service. Multiple targets enable load balancing.
|
|
52
|
+
- **GatewayRoute**: Maps an incoming path/method to a downstream service.
|
|
53
|
+
- **AggregateRequest**: Defines sub-requests for parallel fan-out on a single route.
|
|
54
|
+
- **GatewayPlugin**: Middleware (e.g., apikey, rate_limit, cors) attached to a gateway or route. Mutually exclusive: gateway_id OR route_id.
|
|
55
|
+
- **Collection**: Organizational grouping of routes/services (Pro mode only).
|
|
56
|
+
- **UserProfile**: Contains the user's slug (URL namespace).
|
|
57
|
+
|
|
58
|
+
## Available API Endpoints
|
|
59
|
+
All protected routes require a valid JWT Bearer token.
|
|
60
|
+
|
|
61
|
+
### System
|
|
62
|
+
- \`GET /health\` — Health check
|
|
63
|
+
|
|
64
|
+
### Gateway CRUD (/api/v1/gateways)
|
|
65
|
+
- POST / — Create gateway
|
|
66
|
+
- GET / — List gateways
|
|
67
|
+
- PATCH /:id — Update gateway
|
|
68
|
+
- DELETE /:id — Delete gateway
|
|
69
|
+
|
|
70
|
+
### Service CRUD (/api/v1/services)
|
|
71
|
+
- POST / — Create service (requires gateway_id)
|
|
72
|
+
- GET / — List services
|
|
73
|
+
- PATCH /:id — Update service
|
|
74
|
+
- DELETE /:id — Delete service
|
|
75
|
+
|
|
76
|
+
### Service Targets CRUD (/api/v1/service-targets)
|
|
77
|
+
- POST / — Create target (requires service_id)
|
|
78
|
+
- GET / — List targets
|
|
79
|
+
- PATCH /:id — Update target
|
|
80
|
+
- DELETE /:id — Delete target
|
|
81
|
+
|
|
82
|
+
### Gateway Routes CRUD (/api/v1/gateway-routes)
|
|
83
|
+
- POST / — Create route (requires gateway_id + service_id)
|
|
84
|
+
- GET / — List routes
|
|
85
|
+
- PATCH /:id — Update route
|
|
86
|
+
- DELETE /:id — Delete route
|
|
87
|
+
|
|
88
|
+
### Aggregate Requests CRUD (/api/v1/aggregate-requests)
|
|
89
|
+
- POST / — Create aggregate request (requires route_id + service_id)
|
|
90
|
+
- GET / — List aggregate requests
|
|
91
|
+
- PATCH /:id — Update aggregate request
|
|
92
|
+
- DELETE /:id — Delete aggregate request
|
|
93
|
+
|
|
94
|
+
### Gateway Plugins CRUD (/api/v1/gateway-plugins)
|
|
95
|
+
- POST / — Create plugin (requires gateway_id XOR route_id)
|
|
96
|
+
- GET / — List plugins
|
|
97
|
+
- PATCH /:id — Update plugin
|
|
98
|
+
- DELETE /:id — Delete plugin
|
|
99
|
+
|
|
100
|
+
### Collections CRUD (/api/v1/collections)
|
|
101
|
+
- POST / — Create collection (requires gateway_id)
|
|
102
|
+
- GET / — List collections
|
|
103
|
+
- PATCH /:id — Update collection
|
|
104
|
+
- DELETE /:id — Delete collection
|
|
105
|
+
|
|
106
|
+
### User Profiles (/api/v1/user-profiles)
|
|
107
|
+
- POST / — Create profile (set slug)
|
|
108
|
+
- GET / — Get profile
|
|
109
|
+
- PATCH / — Update slug
|
|
110
|
+
- DELETE / — Delete profile
|
|
111
|
+
|
|
112
|
+
### Logs & Metrics (/api/v1/logs)
|
|
113
|
+
- GET /requests — Recent request logs
|
|
114
|
+
- GET /hourly-metrics — Hourly aggregated metrics
|
|
115
|
+
- GET /hourly-metrics-mv — Hourly metrics (materialized view)
|
|
116
|
+
|
|
117
|
+
### Stats (/api/v1/stats)
|
|
118
|
+
- GET /resources — Resource counts (gateways, services, routes, plugins)
|
|
119
|
+
`,
|
|
120
|
+
}],
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// ── Dynamic Resource: Current Gateway Config ──────────────────
|
|
125
|
+
server.resource(
|
|
126
|
+
'sopo-gateway-config',
|
|
127
|
+
'sopo://gateways/current-config',
|
|
128
|
+
{
|
|
129
|
+
description: 'Live snapshot of all gateways and their configuration owned by the authenticated user.',
|
|
130
|
+
mimeType: 'application/json',
|
|
131
|
+
},
|
|
132
|
+
async () => {
|
|
133
|
+
const result = await sopoGet('/api/v1/gateways');
|
|
134
|
+
return {
|
|
135
|
+
contents: [{
|
|
136
|
+
uri: 'sopo://gateways/current-config',
|
|
137
|
+
mimeType: 'application/json',
|
|
138
|
+
text: JSON.stringify(result.success ? result.data : { error: result.error }, null, 2),
|
|
139
|
+
}],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// ── Dynamic Resource: Current Stats ───────────────────────────
|
|
145
|
+
server.resource(
|
|
146
|
+
'sopo-resource-stats',
|
|
147
|
+
'sopo://stats/resources',
|
|
148
|
+
{
|
|
149
|
+
description: 'Live resource counts for the authenticated user (gateways, services, routes, plugins).',
|
|
150
|
+
mimeType: 'application/json',
|
|
151
|
+
},
|
|
152
|
+
async () => {
|
|
153
|
+
const result = await sopoGet('/api/v1/stats/resources');
|
|
154
|
+
return {
|
|
155
|
+
contents: [{
|
|
156
|
+
uri: 'sopo://stats/resources',
|
|
157
|
+
mimeType: 'application/json',
|
|
158
|
+
text: JSON.stringify(result.success ? result.data : { error: result.error }, null, 2),
|
|
159
|
+
}],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sopo MCP Server — HTTP Client
|
|
3
|
+
*
|
|
4
|
+
* Handles all communication with the Sopo Backend REST API.
|
|
5
|
+
* Manages authentication headers and error normalization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SopoConfig {
|
|
9
|
+
backendUrl: string;
|
|
10
|
+
accessToken: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SopoResponse<T = any> {
|
|
14
|
+
success: boolean;
|
|
15
|
+
data?: T;
|
|
16
|
+
error?: string;
|
|
17
|
+
statusCode: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let config: SopoConfig | null = null;
|
|
21
|
+
let storedRefreshToken: string | null = null;
|
|
22
|
+
|
|
23
|
+
export function configureSopoClient(cfg: SopoConfig): void {
|
|
24
|
+
config = cfg;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Updates the access token at runtime (e.g., after login or token refresh).
|
|
29
|
+
* All subsequent API calls will use the new token.
|
|
30
|
+
*/
|
|
31
|
+
export function updateAccessToken(newToken: string): void {
|
|
32
|
+
if (!config) {
|
|
33
|
+
throw new Error('Sopo client not configured. Call configureSopoClient() first.');
|
|
34
|
+
}
|
|
35
|
+
config.accessToken = newToken;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stores the refresh token in memory for automatic token refresh.
|
|
40
|
+
*/
|
|
41
|
+
export function setRefreshToken(token: string): void {
|
|
42
|
+
storedRefreshToken = token;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Retrieves the stored refresh token (if any).
|
|
47
|
+
*/
|
|
48
|
+
export function getRefreshToken(): string | null {
|
|
49
|
+
return storedRefreshToken;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getConfig(): SopoConfig {
|
|
53
|
+
if (!config) {
|
|
54
|
+
throw new Error('Sopo client not configured. Call configureSopoClient() first.');
|
|
55
|
+
}
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the backend base URL for direct fetch calls (e.g., auth endpoints).
|
|
61
|
+
*/
|
|
62
|
+
export function getBackendUrl(): string {
|
|
63
|
+
return getConfig().backendUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildHeaders(): Record<string, string> {
|
|
67
|
+
const cfg = getConfig();
|
|
68
|
+
return {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Authorization': cfg.accessToken.startsWith('Bearer ')
|
|
71
|
+
? cfg.accessToken
|
|
72
|
+
: `Bearer ${cfg.accessToken}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Performs a GET request to the Sopo Backend.
|
|
78
|
+
*/
|
|
79
|
+
export async function sopoGet<T = any>(path: string): Promise<SopoResponse<T>> {
|
|
80
|
+
const cfg = getConfig();
|
|
81
|
+
const url = `${cfg.backendUrl}${path}`;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
method: 'GET',
|
|
86
|
+
headers: buildHeaders(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const body = await response.json().catch(() => null);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errorMsg = body?.error?.message || body?.message || `HTTP ${response.status}`;
|
|
93
|
+
return { success: false, error: errorMsg, statusCode: response.status };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { success: true, data: body as T, statusCode: response.status };
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
return { success: false, error: err.message || 'Network error', statusCode: 0 };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Performs a POST request to the Sopo Backend.
|
|
104
|
+
*/
|
|
105
|
+
export async function sopoPost<T = any>(path: string, body: any): Promise<SopoResponse<T>> {
|
|
106
|
+
const cfg = getConfig();
|
|
107
|
+
const url = `${cfg.backendUrl}${path}`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: buildHeaders(),
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const responseBody = await response.json().catch(() => null);
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const errorMsg = responseBody?.error?.message || responseBody?.message || `HTTP ${response.status}`;
|
|
120
|
+
return { success: false, error: errorMsg, statusCode: response.status };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: true, data: responseBody as T, statusCode: response.status };
|
|
124
|
+
} catch (err: any) {
|
|
125
|
+
return { success: false, error: err.message || 'Network error', statusCode: 0 };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Performs a PATCH request to the Sopo Backend.
|
|
131
|
+
*/
|
|
132
|
+
export async function sopoPatch<T = any>(path: string, body: any): Promise<SopoResponse<T>> {
|
|
133
|
+
const cfg = getConfig();
|
|
134
|
+
const url = `${cfg.backendUrl}${path}`;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(url, {
|
|
138
|
+
method: 'PATCH',
|
|
139
|
+
headers: buildHeaders(),
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const responseBody = await response.json().catch(() => null);
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const errorMsg = responseBody?.error?.message || responseBody?.message || `HTTP ${response.status}`;
|
|
147
|
+
return { success: false, error: errorMsg, statusCode: response.status };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { success: true, data: responseBody as T, statusCode: response.status };
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
return { success: false, error: err.message || 'Network error', statusCode: 0 };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Performs a DELETE request to the Sopo Backend.
|
|
158
|
+
*/
|
|
159
|
+
export async function sopoDelete<T = any>(path: string): Promise<SopoResponse<T>> {
|
|
160
|
+
const cfg = getConfig();
|
|
161
|
+
const url = `${cfg.backendUrl}${path}`;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(url, {
|
|
165
|
+
method: 'DELETE',
|
|
166
|
+
headers: buildHeaders(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const responseBody = await response.json().catch(() => null);
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const errorMsg = responseBody?.error?.message || responseBody?.message || `HTTP ${response.status}`;
|
|
173
|
+
return { success: false, error: errorMsg, statusCode: response.status };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { success: true, data: responseBody as T, statusCode: response.status };
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
return { success: false, error: err.message || 'Network error', statusCode: 0 };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sopo MCP Server — Aggregate Request Tools
|
|
3
|
+
*
|
|
4
|
+
* CRUD tools for managing Aggregate Requests (parallel sub-requests on a single route).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { sopoGet, sopoPost, sopoPatch, sopoDelete } from '../sopo-client.js';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
export function registerAggregateRequestTools(server: McpServer): void {
|
|
12
|
+
|
|
13
|
+
// ── List Aggregate Requests ───────────────────────────────────
|
|
14
|
+
server.tool(
|
|
15
|
+
'list_aggregate_requests',
|
|
16
|
+
'List all aggregate request configurations. Returns id, route_id, service_id, key_name, method, target_path, and timeout.',
|
|
17
|
+
{},
|
|
18
|
+
async () => {
|
|
19
|
+
const result = await sopoGet('/api/v1/aggregate-requests');
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ── Create Aggregate Request ──────────────────────────────────
|
|
28
|
+
server.tool(
|
|
29
|
+
'create_aggregate_request',
|
|
30
|
+
'Define a sub-request for parallel execution on a gateway route. Requires route_id, service_id, and key_name. Both IDs must reference existing resources.',
|
|
31
|
+
{
|
|
32
|
+
route_id: z.string().uuid().describe('UUID of the parent gateway route (must exist)'),
|
|
33
|
+
service_id: z.string().uuid().describe('UUID of the service to call (must exist)'),
|
|
34
|
+
key_name: z.string().describe('Key name for the response merge (e.g. "users")'),
|
|
35
|
+
method: z.string().optional().describe('HTTP method for this sub-request'),
|
|
36
|
+
target_path: z.string().optional().describe('Target path for this sub-request'),
|
|
37
|
+
timeout: z.string().optional().describe('Timeout for this sub-request'),
|
|
38
|
+
},
|
|
39
|
+
async (args) => {
|
|
40
|
+
const result = await sopoPost('/api/v1/aggregate-requests', args);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
return { content: [{ type: 'text', text: `Error creating aggregate request: ${result.error}` }], isError: true };
|
|
43
|
+
}
|
|
44
|
+
return { content: [{ type: 'text', text: `Aggregate request created:\n${JSON.stringify(result.data, null, 2)}` }] };
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// ── Update Aggregate Request ──────────────────────────────────
|
|
49
|
+
server.tool(
|
|
50
|
+
'update_aggregate_request',
|
|
51
|
+
'Update an aggregate request configuration by UUID.',
|
|
52
|
+
{
|
|
53
|
+
id: z.string().uuid().describe('UUID of the aggregate request'),
|
|
54
|
+
method: z.string().optional().describe('New HTTP method'),
|
|
55
|
+
target_path: z.string().optional().describe('New target path'),
|
|
56
|
+
timeout: z.string().optional().describe('New timeout'),
|
|
57
|
+
},
|
|
58
|
+
async (args) => {
|
|
59
|
+
const { id, ...body } = args;
|
|
60
|
+
const result = await sopoPatch(`/api/v1/aggregate-requests/${id}`, body);
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
return { content: [{ type: 'text', text: `Error updating aggregate request: ${result.error}` }], isError: true };
|
|
63
|
+
}
|
|
64
|
+
return { content: [{ type: 'text', text: `Aggregate request updated:\n${JSON.stringify(result.data, null, 2)}` }] };
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ── Delete Aggregate Request ──────────────────────────────────
|
|
69
|
+
server.tool(
|
|
70
|
+
'delete_aggregate_request',
|
|
71
|
+
'Delete an aggregate request by its UUID.',
|
|
72
|
+
{
|
|
73
|
+
id: z.string().uuid().describe('UUID of the aggregate request to delete'),
|
|
74
|
+
},
|
|
75
|
+
async (args) => {
|
|
76
|
+
const result = await sopoDelete(`/api/v1/aggregate-requests/${args.id}`);
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
return { content: [{ type: 'text', text: `Error deleting aggregate request: ${result.error}` }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
return { content: [{ type: 'text', text: `Aggregate request deleted successfully.` }] };
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sopo MCP Server — Authentication Tools
|
|
3
|
+
*
|
|
4
|
+
* Provides login and token refresh capabilities so the AI can
|
|
5
|
+
* authenticate automatically without requiring manual token setup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
getBackendUrl,
|
|
12
|
+
updateAccessToken,
|
|
13
|
+
setRefreshToken,
|
|
14
|
+
getRefreshToken,
|
|
15
|
+
} from '../sopo-client.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers authentication tools on the MCP server.
|
|
19
|
+
*/
|
|
20
|
+
export function registerAuthTools(server: McpServer): void {
|
|
21
|
+
|
|
22
|
+
// ── Login with Email & Password ──────────────────────────────
|
|
23
|
+
server.tool(
|
|
24
|
+
'sopo_login',
|
|
25
|
+
'Sign in to Sopo with email and password. On success, the access token is automatically configured for all subsequent API calls. You do NOT need to set any token manually after this.',
|
|
26
|
+
{
|
|
27
|
+
email: z.string().email().describe('User email address'),
|
|
28
|
+
password: z.string().min(1).describe('User password'),
|
|
29
|
+
},
|
|
30
|
+
async ({ email, password }) => {
|
|
31
|
+
try {
|
|
32
|
+
const backendUrl = getBackendUrl();
|
|
33
|
+
const response = await fetch(`${backendUrl}/auth/signin/email-password`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ email, password }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const body = await response.json().catch(() => null);
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorMsg = body?.error?.message || body?.message || `HTTP ${response.status}`;
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text' as const, text: `❌ Login failed: ${errorMsg}` }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract access token from response
|
|
49
|
+
const accessToken = body?.session?.accessToken;
|
|
50
|
+
if (!accessToken) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text' as const, text: '❌ Login succeeded but no access token was returned. Response: ' + JSON.stringify(body) }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Update the client with the new access token
|
|
57
|
+
updateAccessToken(accessToken);
|
|
58
|
+
|
|
59
|
+
// Try to extract refresh token from response body or Set-Cookie header
|
|
60
|
+
const refreshTokenFromBody = body?.session?.refreshToken;
|
|
61
|
+
const setCookieHeader = response.headers.get('set-cookie');
|
|
62
|
+
let refreshTokenValue = refreshTokenFromBody;
|
|
63
|
+
|
|
64
|
+
if (!refreshTokenValue && setCookieHeader) {
|
|
65
|
+
// Parse sopo_refresh_token from Set-Cookie header
|
|
66
|
+
const match = setCookieHeader.match(/sopo_refresh_token=([^;]+)/);
|
|
67
|
+
if (match) {
|
|
68
|
+
refreshTokenValue = match[1];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (refreshTokenValue) {
|
|
73
|
+
setRefreshToken(refreshTokenValue);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const userName = body?.session?.user?.displayName || body?.session?.user?.email || email;
|
|
77
|
+
const expiresIn = body?.session?.accessTokenExpiresIn;
|
|
78
|
+
const hasRefresh = !!refreshTokenValue;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
content: [{
|
|
82
|
+
type: 'text' as const,
|
|
83
|
+
text: `✅ Login successful!\n\n` +
|
|
84
|
+
`👤 User: ${userName}\n` +
|
|
85
|
+
`🔑 Access Token: configured automatically\n` +
|
|
86
|
+
`⏱️ Expires in: ${expiresIn ? `${expiresIn} seconds` : 'unknown'}\n` +
|
|
87
|
+
`🔄 Refresh Token: ${hasRefresh ? 'stored (auto-refresh available)' : 'not available (login again when token expires)'}\n\n` +
|
|
88
|
+
`All API calls will now use this session. You can start managing gateways, services, and routes.`,
|
|
89
|
+
}],
|
|
90
|
+
};
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: 'text' as const, text: `❌ Login error: ${err.message || 'Network error'}` }],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── Refresh Access Token ─────────────────────────────────────
|
|
100
|
+
server.tool(
|
|
101
|
+
'sopo_refresh_token',
|
|
102
|
+
'Refresh the current access token using the stored refresh token. Call this when API calls start returning 401 Unauthorized errors. The new access token is automatically configured.',
|
|
103
|
+
{},
|
|
104
|
+
async () => {
|
|
105
|
+
try {
|
|
106
|
+
const currentRefreshToken = getRefreshToken();
|
|
107
|
+
|
|
108
|
+
if (!currentRefreshToken) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: 'text' as const,
|
|
112
|
+
text: '❌ No refresh token available. Please use `sopo_login` to sign in first.',
|
|
113
|
+
}],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const backendUrl = getBackendUrl();
|
|
118
|
+
const response = await fetch(`${backendUrl}/auth/token`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ refreshToken: currentRefreshToken }),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const body = await response.json().catch(() => null);
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const errorMsg = body?.error?.message || body?.message || `HTTP ${response.status}`;
|
|
128
|
+
// Clear the stored refresh token since it's invalid
|
|
129
|
+
setRefreshToken('');
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text' as const,
|
|
133
|
+
text: `❌ Token refresh failed: ${errorMsg}\n\nPlease use \`sopo_login\` to sign in again.`,
|
|
134
|
+
}],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Extract new access token
|
|
139
|
+
const accessToken = body?.session?.accessToken || body?.accessToken;
|
|
140
|
+
if (!accessToken) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{
|
|
143
|
+
type: 'text' as const,
|
|
144
|
+
text: '❌ Token refresh succeeded but no access token was returned. Response: ' + JSON.stringify(body),
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update the client with the new access token
|
|
150
|
+
updateAccessToken(accessToken);
|
|
151
|
+
|
|
152
|
+
// Update refresh token if a new one was provided
|
|
153
|
+
const newRefreshToken = body?.session?.refreshToken || body?.refreshToken;
|
|
154
|
+
const setCookieHeader = response.headers.get('set-cookie');
|
|
155
|
+
|
|
156
|
+
if (newRefreshToken) {
|
|
157
|
+
setRefreshToken(newRefreshToken);
|
|
158
|
+
} else if (setCookieHeader) {
|
|
159
|
+
const match = setCookieHeader.match(/sopo_refresh_token=([^;]+)/);
|
|
160
|
+
if (match) {
|
|
161
|
+
setRefreshToken(match[1]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const expiresIn = body?.session?.accessTokenExpiresIn || body?.accessTokenExpiresIn;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: 'text' as const,
|
|
170
|
+
text: `✅ Token refreshed successfully!\n\n` +
|
|
171
|
+
`🔑 New Access Token: configured automatically\n` +
|
|
172
|
+
`⏱️ Expires in: ${expiresIn ? `${expiresIn} seconds` : 'unknown'}\n` +
|
|
173
|
+
`🔄 Refresh Token: updated\n\n` +
|
|
174
|
+
`All API calls will now use the new token.`,
|
|
175
|
+
}],
|
|
176
|
+
};
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: 'text' as const,
|
|
181
|
+
text: `❌ Token refresh error: ${err.message || 'Network error'}`,
|
|
182
|
+
}],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sopo MCP Server — Collection Tools
|
|
3
|
+
*
|
|
4
|
+
* CRUD tools for managing Collections (organizational groups for routes/services in Pro mode).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { sopoGet, sopoPost, sopoPatch, sopoDelete } from '../sopo-client.js';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
export function registerCollectionTools(server: McpServer): void {
|
|
12
|
+
|
|
13
|
+
// ── List Collections ──────────────────────────────────────────
|
|
14
|
+
server.tool(
|
|
15
|
+
'list_collections',
|
|
16
|
+
'List all collections (organizational groupings for Pro mode gateways). Returns id, gateway_id, name, and description.',
|
|
17
|
+
{},
|
|
18
|
+
async () => {
|
|
19
|
+
const result = await sopoGet('/api/v1/collections');
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ── Create Collection ─────────────────────────────────────────
|
|
28
|
+
server.tool(
|
|
29
|
+
'create_collection',
|
|
30
|
+
'Create a new collection inside a gateway (Pro mode). Requires gateway_id and name.',
|
|
31
|
+
{
|
|
32
|
+
gateway_id: z.string().uuid().describe('UUID of the parent gateway (must exist)'),
|
|
33
|
+
name: z.string().describe('Collection name (e.g. "v1", "auth-apis")'),
|
|
34
|
+
description: z.string().optional().describe('Description of the collection'),
|
|
35
|
+
},
|
|
36
|
+
async (args) => {
|
|
37
|
+
const result = await sopoPost('/api/v1/collections', args);
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
return { content: [{ type: 'text', text: `Error creating collection: ${result.error}` }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
return { content: [{ type: 'text', text: `Collection created:\n${JSON.stringify(result.data, null, 2)}` }] };
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ── Update Collection ─────────────────────────────────────────
|
|
46
|
+
server.tool(
|
|
47
|
+
'update_collection',
|
|
48
|
+
'Update a collection by UUID. You can change name, description, or is_active.',
|
|
49
|
+
{
|
|
50
|
+
id: z.string().uuid().describe('UUID of the collection'),
|
|
51
|
+
name: z.string().optional().describe('New name'),
|
|
52
|
+
description: z.string().optional().describe('New description'),
|
|
53
|
+
is_active: z.boolean().optional().describe('Active status'),
|
|
54
|
+
},
|
|
55
|
+
async (args) => {
|
|
56
|
+
const { id, ...body } = args;
|
|
57
|
+
const result = await sopoPatch(`/api/v1/collections/${id}`, body);
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
return { content: [{ type: 'text', text: `Error updating collection: ${result.error}` }], isError: true };
|
|
60
|
+
}
|
|
61
|
+
return { content: [{ type: 'text', text: `Collection updated:\n${JSON.stringify(result.data, null, 2)}` }] };
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ── Delete Collection ─────────────────────────────────────────
|
|
66
|
+
server.tool(
|
|
67
|
+
'delete_collection',
|
|
68
|
+
'Delete a collection by its UUID.',
|
|
69
|
+
{
|
|
70
|
+
id: z.string().uuid().describe('UUID of the collection to delete'),
|
|
71
|
+
},
|
|
72
|
+
async (args) => {
|
|
73
|
+
const result = await sopoDelete(`/api/v1/collections/${args.id}`);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
return { content: [{ type: 'text', text: `Error deleting collection: ${result.error}` }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
return { content: [{ type: 'text', text: `Collection deleted successfully.` }] };
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|