@vucinatim/agentic-devtools 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/LICENSE +21 -0
- package/README.md +202 -0
- package/SECURITY.md +47 -0
- package/adapters/claude/namecheap/README.md +13 -0
- package/adapters/claude/npm/README.md +11 -0
- package/adapters/claude/railway/README.md +11 -0
- package/adapters/codex/namecheap/.codex-plugin/plugin.json +40 -0
- package/adapters/codex/namecheap/.mcp.json +21 -0
- package/adapters/codex/namecheap/SKILL.md +40 -0
- package/adapters/codex/npm/.codex-plugin/plugin.json +41 -0
- package/adapters/codex/npm/.mcp.json +18 -0
- package/adapters/codex/npm/SKILL.md +54 -0
- package/adapters/codex/railway/.codex-plugin/plugin.json +39 -0
- package/adapters/codex/railway/.mcp.json +20 -0
- package/adapters/codex/railway/SKILL.md +44 -0
- package/docs/README.md +14 -0
- package/docs/architecture.md +208 -0
- package/docs/auth-and-setup-guidelines.md +261 -0
- package/docs/migration-plan.md +55 -0
- package/docs/open-source-readiness.md +119 -0
- package/docs/publishing.md +211 -0
- package/docs/testing.md +61 -0
- package/docs/usage.md +144 -0
- package/package.json +78 -0
- package/src/cli.mjs +158 -0
- package/src/core/config-store.mjs +106 -0
- package/src/core/result.mjs +13 -0
- package/src/core/tool-registry.mjs +29 -0
- package/src/index.mjs +47 -0
- package/src/tools/namecheap/auth.mjs +429 -0
- package/src/tools/namecheap/client.mjs +655 -0
- package/src/tools/namecheap/mcp.mjs +298 -0
- package/src/tools/npm/auth.mjs +367 -0
- package/src/tools/npm/client.mjs +317 -0
- package/src/tools/npm/mcp.mjs +343 -0
- package/src/tools/railway/auth.mjs +402 -0
- package/src/tools/railway/client.mjs +388 -0
- package/src/tools/railway/mcp.mjs +282 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RAILWAY_API_ENDPOINT,
|
|
3
|
+
getRailwayAuthStatus,
|
|
4
|
+
resolveRailwayApiToken,
|
|
5
|
+
} from "./auth.mjs";
|
|
6
|
+
|
|
7
|
+
export { getRailwayAuthStatus, resolveRailwayApiToken } from "./auth.mjs";
|
|
8
|
+
|
|
9
|
+
export class RailwayApiError extends Error {
|
|
10
|
+
constructor(message, details = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "RailwayApiError";
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createRailwayClient = ({
|
|
18
|
+
env = process.env,
|
|
19
|
+
fetchImpl = globalThis.fetch,
|
|
20
|
+
} = {}) => {
|
|
21
|
+
const auth = resolveRailwayApiToken(env);
|
|
22
|
+
const status = getRailwayAuthStatus(env);
|
|
23
|
+
const endpoint = status.endpoint || DEFAULT_RAILWAY_API_ENDPOINT;
|
|
24
|
+
|
|
25
|
+
if (typeof fetchImpl !== "function") {
|
|
26
|
+
throw new Error("Railway client requires a fetch implementation.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const request = async (query, variables = {}) => {
|
|
30
|
+
if (!auth.token) {
|
|
31
|
+
throw new RailwayApiError(
|
|
32
|
+
"Missing Railway API token. Run `agentic-devtools connect railway`, or set RAILWAY_PROJECT_TOKEN, RAILWAY_API_TOKEN, or RAILWAY_TOKEN.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await fetchImpl(endpoint, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
...(auth.kind === "project"
|
|
41
|
+
? { "Project-Access-Token": auth.token }
|
|
42
|
+
: { Authorization: `Bearer ${auth.token}` }),
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
query,
|
|
46
|
+
variables,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const payload = await response.json().catch(async () => response.text());
|
|
51
|
+
if (!response.ok || hasErrors(payload)) {
|
|
52
|
+
throw new RailwayApiError(
|
|
53
|
+
formatRailwayErrorMessage(payload, response.status),
|
|
54
|
+
{
|
|
55
|
+
status: response.status,
|
|
56
|
+
errors: hasErrors(payload) ? payload.errors : [],
|
|
57
|
+
payload,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return payload.data;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const requireAccountToken = (operation) => {
|
|
66
|
+
if (auth.kind === "project") {
|
|
67
|
+
throw new RailwayApiError(
|
|
68
|
+
`${operation} requires an account token. Set RAILWAY_API_TOKEN or RAILWAY_TOKEN, or use a project-scoped tool.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const resolveProjectId = async (projectId) => {
|
|
74
|
+
const explicit = projectId?.trim() || status.defaultProjectId;
|
|
75
|
+
if (explicit) {
|
|
76
|
+
return explicit;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (auth.kind === "project") {
|
|
80
|
+
const context = await getProjectTokenContext();
|
|
81
|
+
return context.projectId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new RailwayApiError(
|
|
85
|
+
"Missing Railway project id. Pass projectId, set RAILWAY_PROJECT_ID, or use RAILWAY_PROJECT_TOKEN.",
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getCurrentViewer = async () => {
|
|
90
|
+
requireAccountToken("getRailwayViewer");
|
|
91
|
+
const data = await request(`
|
|
92
|
+
query RailwayViewer {
|
|
93
|
+
me {
|
|
94
|
+
name
|
|
95
|
+
email
|
|
96
|
+
workspaces {
|
|
97
|
+
id
|
|
98
|
+
name
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
return data.me;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const listProjects = async ({
|
|
107
|
+
workspaceId = null,
|
|
108
|
+
includeDeleted = false,
|
|
109
|
+
first = 100,
|
|
110
|
+
} = {}) => {
|
|
111
|
+
requireAccountToken("listRailwayProjects");
|
|
112
|
+
const data = await request(
|
|
113
|
+
`
|
|
114
|
+
query RailwayProjects($workspaceId: String, $includeDeleted: Boolean, $first: Int) {
|
|
115
|
+
projects(
|
|
116
|
+
workspaceId: $workspaceId
|
|
117
|
+
includeDeleted: $includeDeleted
|
|
118
|
+
first: $first
|
|
119
|
+
) {
|
|
120
|
+
edges {
|
|
121
|
+
node {
|
|
122
|
+
id
|
|
123
|
+
name
|
|
124
|
+
updatedAt
|
|
125
|
+
deletedAt
|
|
126
|
+
workspace {
|
|
127
|
+
id
|
|
128
|
+
name
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
{ workspaceId, includeDeleted, first },
|
|
136
|
+
);
|
|
137
|
+
return connectionNodes(data.projects);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getProjectTokenContext = async () => {
|
|
141
|
+
const data = await request(`
|
|
142
|
+
query RailwayProjectTokenContext {
|
|
143
|
+
projectToken {
|
|
144
|
+
id
|
|
145
|
+
name
|
|
146
|
+
projectId
|
|
147
|
+
environmentId
|
|
148
|
+
project {
|
|
149
|
+
id
|
|
150
|
+
name
|
|
151
|
+
workspace {
|
|
152
|
+
id
|
|
153
|
+
name
|
|
154
|
+
}
|
|
155
|
+
baseEnvironmentId
|
|
156
|
+
primaryEnvironmentId
|
|
157
|
+
}
|
|
158
|
+
environment {
|
|
159
|
+
id
|
|
160
|
+
name
|
|
161
|
+
projectId
|
|
162
|
+
isEphemeral
|
|
163
|
+
canAccess
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
`);
|
|
168
|
+
return data.projectToken;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const getProject = async (projectId) => {
|
|
172
|
+
const id = await resolveProjectId(projectId);
|
|
173
|
+
const data = await request(
|
|
174
|
+
`
|
|
175
|
+
query RailwayProject($id: String!) {
|
|
176
|
+
project(id: $id) {
|
|
177
|
+
id
|
|
178
|
+
name
|
|
179
|
+
prDeploys
|
|
180
|
+
focusedPrEnvironments
|
|
181
|
+
botPrEnvironments
|
|
182
|
+
baseEnvironmentId
|
|
183
|
+
primaryEnvironmentId
|
|
184
|
+
workspace {
|
|
185
|
+
id
|
|
186
|
+
name
|
|
187
|
+
}
|
|
188
|
+
environments {
|
|
189
|
+
edges {
|
|
190
|
+
node {
|
|
191
|
+
id
|
|
192
|
+
name
|
|
193
|
+
isEphemeral
|
|
194
|
+
canAccess
|
|
195
|
+
projectId
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
services {
|
|
200
|
+
edges {
|
|
201
|
+
node {
|
|
202
|
+
id
|
|
203
|
+
name
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`,
|
|
210
|
+
{ id },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...data.project,
|
|
215
|
+
environments: connectionNodes(data.project.environments),
|
|
216
|
+
services: connectionNodes(data.project.services),
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const listEnvironments = async ({ projectId, isEphemeral } = {}) => {
|
|
221
|
+
const resolvedProjectId = await resolveProjectId(projectId);
|
|
222
|
+
const data = await request(
|
|
223
|
+
`
|
|
224
|
+
query RailwayEnvironments($projectId: String!, $isEphemeral: Boolean) {
|
|
225
|
+
environments(projectId: $projectId, isEphemeral: $isEphemeral) {
|
|
226
|
+
edges {
|
|
227
|
+
node {
|
|
228
|
+
id
|
|
229
|
+
name
|
|
230
|
+
isEphemeral
|
|
231
|
+
canAccess
|
|
232
|
+
projectId
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
`,
|
|
238
|
+
{ projectId: resolvedProjectId, isEphemeral },
|
|
239
|
+
);
|
|
240
|
+
return connectionNodes(data.environments);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const getEnvironment = async (environmentId) => {
|
|
244
|
+
const data = await request(
|
|
245
|
+
`
|
|
246
|
+
query RailwayEnvironment($id: String!) {
|
|
247
|
+
environment(id: $id) {
|
|
248
|
+
id
|
|
249
|
+
name
|
|
250
|
+
isEphemeral
|
|
251
|
+
canAccess
|
|
252
|
+
projectId
|
|
253
|
+
sourceEnvironment {
|
|
254
|
+
id
|
|
255
|
+
name
|
|
256
|
+
}
|
|
257
|
+
serviceInstances {
|
|
258
|
+
edges {
|
|
259
|
+
node {
|
|
260
|
+
id
|
|
261
|
+
environmentId
|
|
262
|
+
serviceId
|
|
263
|
+
serviceName
|
|
264
|
+
rootDirectory
|
|
265
|
+
railwayConfigFile
|
|
266
|
+
startCommand
|
|
267
|
+
healthcheckPath
|
|
268
|
+
latestDeployment {
|
|
269
|
+
id
|
|
270
|
+
status
|
|
271
|
+
url
|
|
272
|
+
staticUrl
|
|
273
|
+
}
|
|
274
|
+
domains {
|
|
275
|
+
serviceDomains {
|
|
276
|
+
domain
|
|
277
|
+
}
|
|
278
|
+
customDomains {
|
|
279
|
+
domain
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
`,
|
|
288
|
+
{ id: environmentId },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
...data.environment,
|
|
293
|
+
serviceInstances: connectionNodes(data.environment.serviceInstances),
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const doctorProject = async ({ projectId } = {}) => {
|
|
298
|
+
const project = await getProject(projectId);
|
|
299
|
+
const primaryEnvironmentId =
|
|
300
|
+
project.primaryEnvironmentId ??
|
|
301
|
+
project.baseEnvironmentId ??
|
|
302
|
+
project.environments.find((entry) => entry.name.toLowerCase() === "production")
|
|
303
|
+
?.id;
|
|
304
|
+
|
|
305
|
+
if (!primaryEnvironmentId) {
|
|
306
|
+
throw new RailwayApiError(
|
|
307
|
+
`Could not resolve primary Railway environment for ${project.name}.`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const environment = await getEnvironment(primaryEnvironmentId);
|
|
312
|
+
return {
|
|
313
|
+
project: {
|
|
314
|
+
id: project.id,
|
|
315
|
+
name: project.name,
|
|
316
|
+
workspace: project.workspace?.name ?? null,
|
|
317
|
+
},
|
|
318
|
+
environment: {
|
|
319
|
+
id: environment.id,
|
|
320
|
+
name: environment.name,
|
|
321
|
+
isEphemeral: environment.isEphemeral,
|
|
322
|
+
},
|
|
323
|
+
services: environment.serviceInstances.map((entry) => ({
|
|
324
|
+
serviceId: entry.serviceId,
|
|
325
|
+
serviceName: entry.serviceName,
|
|
326
|
+
railwayConfigFile: entry.railwayConfigFile ?? null,
|
|
327
|
+
rootDirectory: entry.rootDirectory ?? null,
|
|
328
|
+
startCommand: entry.startCommand ?? null,
|
|
329
|
+
healthcheckPath: entry.healthcheckPath ?? null,
|
|
330
|
+
deployment: entry.latestDeployment
|
|
331
|
+
? {
|
|
332
|
+
id: entry.latestDeployment.id,
|
|
333
|
+
status: entry.latestDeployment.status,
|
|
334
|
+
url: entry.latestDeployment.url ?? null,
|
|
335
|
+
staticUrl: entry.latestDeployment.staticUrl ?? null,
|
|
336
|
+
}
|
|
337
|
+
: null,
|
|
338
|
+
customDomains:
|
|
339
|
+
entry.domains?.customDomains?.map((item) => item.domain) ?? [],
|
|
340
|
+
serviceDomains:
|
|
341
|
+
entry.domains?.serviceDomains?.map((item) => item.domain) ?? [],
|
|
342
|
+
})),
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
auth,
|
|
348
|
+
endpoint,
|
|
349
|
+
getAuthStatus: () => getRailwayAuthStatus(env),
|
|
350
|
+
getCurrentViewer,
|
|
351
|
+
listProjects,
|
|
352
|
+
getProjectTokenContext,
|
|
353
|
+
getProject,
|
|
354
|
+
listEnvironments,
|
|
355
|
+
getEnvironment,
|
|
356
|
+
doctorProject,
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const connectionNodes = (connection) => {
|
|
361
|
+
const nodes = [];
|
|
362
|
+
for (const entry of connection?.edges ?? []) {
|
|
363
|
+
if (entry.node != null) {
|
|
364
|
+
nodes.push(entry.node);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return nodes;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const hasErrors = (payload) =>
|
|
371
|
+
typeof payload === "object" &&
|
|
372
|
+
payload !== null &&
|
|
373
|
+
"errors" in payload &&
|
|
374
|
+
Array.isArray(payload.errors);
|
|
375
|
+
|
|
376
|
+
const formatRailwayErrorMessage = (payload, status) => {
|
|
377
|
+
if (hasErrors(payload)) {
|
|
378
|
+
const joined = payload.errors
|
|
379
|
+
.map((entry) => entry?.message?.trim())
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.join(" | ");
|
|
382
|
+
if (joined) {
|
|
383
|
+
return joined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return `Railway API request failed with HTTP ${status}`;
|
|
388
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
disconnectRailway,
|
|
8
|
+
RAILWAY_AUTH_CONFIG_PATH,
|
|
9
|
+
runRailwayBrowserAuthFlow,
|
|
10
|
+
} from "./auth.mjs";
|
|
11
|
+
import {
|
|
12
|
+
createRailwayClient,
|
|
13
|
+
getRailwayAuthStatus,
|
|
14
|
+
} from "./client.mjs";
|
|
15
|
+
|
|
16
|
+
const HELP_TEXT = `Usage: agentic-devtools mcp railway
|
|
17
|
+
|
|
18
|
+
Railway MCP server
|
|
19
|
+
|
|
20
|
+
Optional environment variables:
|
|
21
|
+
RAILWAY_PROJECT_TOKEN
|
|
22
|
+
RAILWAY_API_TOKEN
|
|
23
|
+
RAILWAY_TOKEN
|
|
24
|
+
RAILWAY_PROJECT_ID
|
|
25
|
+
RAILWAY_API_ENDPOINT
|
|
26
|
+
|
|
27
|
+
Project tokens can inspect a single project. Account tokens can inspect account identity and list projects.
|
|
28
|
+
|
|
29
|
+
If env vars are not provided, use the connectRailway tool to open the browser-based setup flow and save a local Railway token.
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const createToolResult = (value) => ({
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: JSON.stringify(value, null, 2),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
structuredContent: value,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const createServer = () => {
|
|
43
|
+
const server = new McpServer(
|
|
44
|
+
{
|
|
45
|
+
name: "railway",
|
|
46
|
+
version: "0.1.0",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
instructions:
|
|
50
|
+
"Use these tools for read-only Railway project, environment, service, domain, and deployment inspection. Do not attempt deployment or variable mutation through this plugin.",
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const withClient = async (callback) => {
|
|
55
|
+
const client = createRailwayClient();
|
|
56
|
+
return callback(client);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
server.registerTool(
|
|
60
|
+
"getRailwayAuthStatus",
|
|
61
|
+
{
|
|
62
|
+
description:
|
|
63
|
+
"Show whether Railway credentials are configured and which token scope will be used.",
|
|
64
|
+
},
|
|
65
|
+
async () => createToolResult(getRailwayAuthStatus()),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.registerTool(
|
|
69
|
+
"testRailwayConnection",
|
|
70
|
+
{
|
|
71
|
+
description:
|
|
72
|
+
"Verify that the configured Railway credentials can successfully call the API.",
|
|
73
|
+
},
|
|
74
|
+
async () =>
|
|
75
|
+
createToolResult(
|
|
76
|
+
await withClient(async (client) => {
|
|
77
|
+
if (client.auth.kind === "project") {
|
|
78
|
+
const context = await client.getProjectTokenContext();
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
tokenSource: client.auth.source,
|
|
82
|
+
tokenKind: client.auth.kind,
|
|
83
|
+
project: {
|
|
84
|
+
id: context.project.id,
|
|
85
|
+
name: context.project.name,
|
|
86
|
+
},
|
|
87
|
+
environment: {
|
|
88
|
+
id: context.environment.id,
|
|
89
|
+
name: context.environment.name,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const viewer = await client.getCurrentViewer();
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
tokenSource: client.auth.source,
|
|
98
|
+
tokenKind: client.auth.kind,
|
|
99
|
+
viewer: {
|
|
100
|
+
name: viewer.name,
|
|
101
|
+
email: viewer.email,
|
|
102
|
+
workspaceCount: viewer.workspaces.length,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
server.registerTool(
|
|
110
|
+
"connectRailway",
|
|
111
|
+
{
|
|
112
|
+
description:
|
|
113
|
+
"Open a browser-based setup flow for a Railway account or project token and save it locally for this MCP server.",
|
|
114
|
+
},
|
|
115
|
+
async () =>
|
|
116
|
+
createToolResult({
|
|
117
|
+
...(await runRailwayBrowserAuthFlow()),
|
|
118
|
+
configPath: RAILWAY_AUTH_CONFIG_PATH,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.registerTool(
|
|
123
|
+
"disconnectRailway",
|
|
124
|
+
{
|
|
125
|
+
description:
|
|
126
|
+
"Remove the locally stored Railway token from the plugin auth file.",
|
|
127
|
+
},
|
|
128
|
+
async () => createToolResult(await disconnectRailway()),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
server.registerTool(
|
|
132
|
+
"getRailwayViewer",
|
|
133
|
+
{
|
|
134
|
+
description:
|
|
135
|
+
"Inspect the current Railway account identity. Requires RAILWAY_API_TOKEN or RAILWAY_TOKEN.",
|
|
136
|
+
},
|
|
137
|
+
async () =>
|
|
138
|
+
createToolResult(
|
|
139
|
+
await withClient(async (client) => ({
|
|
140
|
+
tokenSource: client.auth.source,
|
|
141
|
+
viewer: await client.getCurrentViewer(),
|
|
142
|
+
})),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.registerTool(
|
|
147
|
+
"listRailwayProjects",
|
|
148
|
+
{
|
|
149
|
+
description:
|
|
150
|
+
"List Railway projects for the configured account token. Requires RAILWAY_API_TOKEN or RAILWAY_TOKEN.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
workspaceId: z.string().min(1).optional(),
|
|
153
|
+
includeDeleted: z.boolean().optional(),
|
|
154
|
+
first: z.number().int().min(1).max(100).optional(),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async (args) =>
|
|
158
|
+
createToolResult(
|
|
159
|
+
await withClient((client) => client.listProjects(args ?? {})),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
server.registerTool(
|
|
164
|
+
"inspectRailwayProjectToken",
|
|
165
|
+
{
|
|
166
|
+
description:
|
|
167
|
+
"Inspect the project and environment attached to the configured RAILWAY_PROJECT_TOKEN.",
|
|
168
|
+
},
|
|
169
|
+
async () =>
|
|
170
|
+
createToolResult(
|
|
171
|
+
await withClient((client) => client.getProjectTokenContext()),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.registerTool(
|
|
176
|
+
"getRailwayProject",
|
|
177
|
+
{
|
|
178
|
+
description:
|
|
179
|
+
"Inspect one Railway project. Pass projectId, set RAILWAY_PROJECT_ID, or use RAILWAY_PROJECT_TOKEN.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
projectId: z.string().min(1).optional(),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async ({ projectId } = {}) =>
|
|
185
|
+
createToolResult(
|
|
186
|
+
await withClient((client) => client.getProject(projectId)),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.registerTool(
|
|
191
|
+
"listRailwayEnvironments",
|
|
192
|
+
{
|
|
193
|
+
description:
|
|
194
|
+
"List environments for one Railway project. Pass projectId, set RAILWAY_PROJECT_ID, or use RAILWAY_PROJECT_TOKEN.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
projectId: z.string().min(1).optional(),
|
|
197
|
+
isEphemeral: z.boolean().optional(),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
async (args) =>
|
|
201
|
+
createToolResult(
|
|
202
|
+
await withClient((client) => client.listEnvironments(args ?? {})),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
server.registerTool(
|
|
207
|
+
"getRailwayEnvironment",
|
|
208
|
+
{
|
|
209
|
+
description:
|
|
210
|
+
"Inspect one Railway environment, including service instances, domains, and latest deployment status.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
environmentId: z.string().min(1),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
async ({ environmentId }) =>
|
|
216
|
+
createToolResult(
|
|
217
|
+
await withClient((client) => client.getEnvironment(environmentId)),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
server.registerTool(
|
|
222
|
+
"doctorRailwayProject",
|
|
223
|
+
{
|
|
224
|
+
description:
|
|
225
|
+
"Return a compact project health summary for the primary Railway environment and service deployments.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
projectId: z.string().min(1).optional(),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
async ({ projectId } = {}) =>
|
|
231
|
+
createToolResult(
|
|
232
|
+
await withClient((client) => client.doctorProject({ projectId })),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return server;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const argv = process.argv.slice(2);
|
|
240
|
+
|
|
241
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
242
|
+
process.stdout.write(HELP_TEXT);
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (argv.includes("--auth-status")) {
|
|
247
|
+
process.stdout.write(`${JSON.stringify(getRailwayAuthStatus(), null, 2)}\n`);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (argv.includes("--connect")) {
|
|
252
|
+
process.stdout.write("Opening Railway browser setup flow...\n");
|
|
253
|
+
const result = await runRailwayBrowserAuthFlow();
|
|
254
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (argv.includes("--test-connection")) {
|
|
259
|
+
const client = createRailwayClient();
|
|
260
|
+
const result =
|
|
261
|
+
client.auth.kind === "project"
|
|
262
|
+
? await client.getProjectTokenContext()
|
|
263
|
+
: await client.getCurrentViewer();
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
`${JSON.stringify(
|
|
266
|
+
{
|
|
267
|
+
ok: true,
|
|
268
|
+
tokenSource: client.auth.source,
|
|
269
|
+
tokenKind: client.auth.kind,
|
|
270
|
+
result,
|
|
271
|
+
},
|
|
272
|
+
null,
|
|
273
|
+
2,
|
|
274
|
+
)}\n`,
|
|
275
|
+
);
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const server = createServer();
|
|
280
|
+
const transport = new StdioServerTransport();
|
|
281
|
+
|
|
282
|
+
await server.connect(transport);
|