@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/SECURITY.md +47 -0
  4. package/adapters/claude/namecheap/README.md +13 -0
  5. package/adapters/claude/npm/README.md +11 -0
  6. package/adapters/claude/railway/README.md +11 -0
  7. package/adapters/codex/namecheap/.codex-plugin/plugin.json +40 -0
  8. package/adapters/codex/namecheap/.mcp.json +21 -0
  9. package/adapters/codex/namecheap/SKILL.md +40 -0
  10. package/adapters/codex/npm/.codex-plugin/plugin.json +41 -0
  11. package/adapters/codex/npm/.mcp.json +18 -0
  12. package/adapters/codex/npm/SKILL.md +54 -0
  13. package/adapters/codex/railway/.codex-plugin/plugin.json +39 -0
  14. package/adapters/codex/railway/.mcp.json +20 -0
  15. package/adapters/codex/railway/SKILL.md +44 -0
  16. package/docs/README.md +14 -0
  17. package/docs/architecture.md +208 -0
  18. package/docs/auth-and-setup-guidelines.md +261 -0
  19. package/docs/migration-plan.md +55 -0
  20. package/docs/open-source-readiness.md +119 -0
  21. package/docs/publishing.md +211 -0
  22. package/docs/testing.md +61 -0
  23. package/docs/usage.md +144 -0
  24. package/package.json +78 -0
  25. package/src/cli.mjs +158 -0
  26. package/src/core/config-store.mjs +106 -0
  27. package/src/core/result.mjs +13 -0
  28. package/src/core/tool-registry.mjs +29 -0
  29. package/src/index.mjs +47 -0
  30. package/src/tools/namecheap/auth.mjs +429 -0
  31. package/src/tools/namecheap/client.mjs +655 -0
  32. package/src/tools/namecheap/mcp.mjs +298 -0
  33. package/src/tools/npm/auth.mjs +367 -0
  34. package/src/tools/npm/client.mjs +317 -0
  35. package/src/tools/npm/mcp.mjs +343 -0
  36. package/src/tools/railway/auth.mjs +402 -0
  37. package/src/tools/railway/client.mjs +388 -0
  38. 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);