@vucinatim/agentic-devtools 0.1.3 → 0.1.4

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 CHANGED
@@ -138,6 +138,21 @@ GitHub Actions Trusted Publishing over local write tokens. Use
138
138
  `setup-publishing npm` to run npm's official Trusted Publishing setup command
139
139
  through the package.
140
140
 
141
+ ### Railway Coverage
142
+
143
+ The Railway tool now targets parity with Railway's documented public API for:
144
+
145
+ - projects and project members
146
+ - environments
147
+ - services and service instances
148
+ - deployments
149
+ - variables
150
+ - Railway-managed and custom domains
151
+ - volumes
152
+
153
+ This does not currently claim parity for undocumented dashboard-only concepts
154
+ such as canvas grouping internals.
155
+
141
156
  Trusted Publishing notes:
142
157
 
143
158
  - npm currently documents Trusted Publishing for GitHub-hosted GitHub Actions,
@@ -159,6 +159,19 @@ Validation guidance:
159
159
  workspace-compatible reads
160
160
  - keep identity-only queries such as `me` as separate optional capabilities
161
161
 
162
+ Current Railway management scope should cover the documented public API for:
163
+
164
+ - projects and project members
165
+ - environments
166
+ - services and service instances
167
+ - deployments
168
+ - variables
169
+ - service domains and custom domains
170
+ - volumes
171
+
172
+ Do not claim parity for dashboard-only or undocumented organization surfaces
173
+ until Railway documents them as stable public API.
174
+
162
175
  ## Namecheap
163
176
 
164
177
  Namecheap is Tier 3.
package/docs/usage.md CHANGED
@@ -157,6 +157,23 @@ Validation behavior:
157
157
  - identity-style `me` queries are not treated as the compatibility baseline for
158
158
  all bearer tokens
159
159
 
160
+ Railway capability coverage:
161
+
162
+ - read and mutate projects
163
+ - list project members
164
+ - read and mutate environments
165
+ - read and mutate services
166
+ - read and mutate service instance settings and limits
167
+ - deploy and redeploy service instances
168
+ - inspect deployments and list deployment history
169
+ - upsert and delete variables
170
+ - create, update, and delete Railway-managed and custom domains
171
+ - create and delete volumes
172
+
173
+ Not currently included as a supported public contract:
174
+
175
+ - undocumented dashboard canvas/group organization internals
176
+
160
177
  Namecheap supports:
161
178
 
162
179
  - guided local setup through `agentic-devtools connect namecheap`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vucinatim/agentic-devtools",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "MCP-first devtools for AI agents.",
6
6
  "type": "module",
@@ -86,6 +86,16 @@ export const createRailwayClient = ({
86
86
  );
87
87
  };
88
88
 
89
+ const requireResolvedProjectId = async (projectId, operation) => {
90
+ try {
91
+ return await resolveProjectId(projectId);
92
+ } catch (error) {
93
+ throw new RailwayApiError(
94
+ error instanceof Error ? error.message : `${operation} requires a Railway project id.`,
95
+ );
96
+ }
97
+ };
98
+
89
99
  const getCurrentViewer = async () => {
90
100
  requireAccountToken("getRailwayViewer");
91
101
  const data = await request(`
@@ -231,6 +241,27 @@ export const createRailwayClient = ({
231
241
  };
232
242
  };
233
243
 
244
+ const getProjectMembers = async (projectId) => {
245
+ requireAccountToken("getRailwayProjectMembers");
246
+ const id = await requireResolvedProjectId(projectId, "getRailwayProjectMembers");
247
+ const data = await request(
248
+ `
249
+ query RailwayProjectMembers($projectId: String!) {
250
+ projectMembers(projectId: $projectId) {
251
+ id
252
+ role
253
+ user {
254
+ name
255
+ email
256
+ }
257
+ }
258
+ }
259
+ `,
260
+ { projectId: id },
261
+ );
262
+ return data.projectMembers;
263
+ };
264
+
234
265
  const listEnvironments = async ({ projectId, isEphemeral } = {}) => {
235
266
  const resolvedProjectId = await resolveProjectId(projectId);
236
267
  const data = await request(
@@ -308,6 +339,675 @@ export const createRailwayClient = ({
308
339
  };
309
340
  };
310
341
 
342
+ const getService = async (serviceId) => {
343
+ const data = await request(
344
+ `
345
+ query RailwayService($id: String!) {
346
+ service(id: $id) {
347
+ id
348
+ name
349
+ icon
350
+ createdAt
351
+ updatedAt
352
+ deletedAt
353
+ featureFlags
354
+ project {
355
+ id
356
+ name
357
+ }
358
+ }
359
+ }
360
+ `,
361
+ { id: serviceId },
362
+ );
363
+ return {
364
+ ...data.service,
365
+ projectId: data.service.project?.id ?? null,
366
+ projectName: data.service.project?.name ?? null,
367
+ };
368
+ };
369
+
370
+ const getServiceInstance = async ({ serviceId, environmentId }) => {
371
+ const data = await request(
372
+ `
373
+ query RailwayServiceInstance($serviceId: String!, $environmentId: String!) {
374
+ serviceInstance(serviceId: $serviceId, environmentId: $environmentId) {
375
+ id
376
+ serviceId
377
+ serviceName
378
+ environmentId
379
+ rootDirectory
380
+ railwayConfigFile
381
+ buildCommand
382
+ startCommand
383
+ healthcheckPath
384
+ cronSchedule
385
+ latestDeployment {
386
+ id
387
+ status
388
+ url
389
+ staticUrl
390
+ }
391
+ domains {
392
+ serviceDomains {
393
+ id
394
+ domain
395
+ }
396
+ customDomains {
397
+ id
398
+ domain
399
+ }
400
+ }
401
+ }
402
+ }
403
+ `,
404
+ { serviceId, environmentId },
405
+ );
406
+ return data.serviceInstance;
407
+ };
408
+
409
+ const getServiceInstanceLimits = async ({ serviceId, environmentId }) => {
410
+ const data = await request(
411
+ `
412
+ query RailwayServiceInstanceLimits($serviceId: String!, $environmentId: String!) {
413
+ serviceInstanceLimits(serviceId: $serviceId, environmentId: $environmentId)
414
+ }
415
+ `,
416
+ { serviceId, environmentId },
417
+ );
418
+ return data.serviceInstanceLimits;
419
+ };
420
+
421
+ const getDeployment = async (deploymentId) => {
422
+ const data = await request(
423
+ `
424
+ query RailwayDeployment($id: String!) {
425
+ deployment(id: $id) {
426
+ id
427
+ status
428
+ createdAt
429
+ updatedAt
430
+ statusUpdatedAt
431
+ canRedeploy
432
+ canRollback
433
+ deploymentStopped
434
+ environmentId
435
+ projectId
436
+ serviceId
437
+ url
438
+ staticUrl
439
+ service {
440
+ id
441
+ name
442
+ }
443
+ environment {
444
+ id
445
+ name
446
+ }
447
+ }
448
+ }
449
+ `,
450
+ { id: deploymentId },
451
+ );
452
+ return {
453
+ ...data.deployment,
454
+ serviceName: data.deployment.service?.name ?? null,
455
+ environmentName: data.deployment.environment?.name ?? null,
456
+ };
457
+ };
458
+
459
+ const listDeployments = async ({
460
+ projectId,
461
+ environmentId,
462
+ serviceId,
463
+ first = 20,
464
+ after = null,
465
+ before = null,
466
+ last = null,
467
+ } = {}) => {
468
+ const resolvedProjectId =
469
+ projectId == null && auth.kind === "project"
470
+ ? await requireResolvedProjectId(null, "listRailwayDeployments")
471
+ : projectId;
472
+
473
+ const input = compactObject({
474
+ projectId: resolvedProjectId,
475
+ environmentId,
476
+ serviceId,
477
+ });
478
+
479
+ const data = await request(
480
+ `
481
+ query RailwayDeployments(
482
+ $input: DeploymentListInput!
483
+ $first: Int
484
+ $after: String
485
+ $before: String
486
+ $last: Int
487
+ ) {
488
+ deployments(
489
+ input: $input
490
+ first: $first
491
+ after: $after
492
+ before: $before
493
+ last: $last
494
+ ) {
495
+ edges {
496
+ node {
497
+ id
498
+ status
499
+ createdAt
500
+ updatedAt
501
+ environmentId
502
+ projectId
503
+ serviceId
504
+ url
505
+ staticUrl
506
+ }
507
+ }
508
+ pageInfo {
509
+ hasNextPage
510
+ hasPreviousPage
511
+ startCursor
512
+ endCursor
513
+ }
514
+ }
515
+ }
516
+ `,
517
+ { input, first, after, before, last },
518
+ );
519
+
520
+ return {
521
+ deployments: connectionNodes(data.deployments),
522
+ pageInfo: data.deployments?.pageInfo ?? null,
523
+ };
524
+ };
525
+
526
+ const createProject = async (input = {}) => {
527
+ requireAccountToken("createRailwayProject");
528
+ const data = await request(
529
+ `
530
+ mutation RailwayProjectCreate($input: ProjectCreateInput!) {
531
+ projectCreate(input: $input) {
532
+ id
533
+ name
534
+ description
535
+ workspace {
536
+ id
537
+ name
538
+ }
539
+ }
540
+ }
541
+ `,
542
+ { input: compactObject(input) },
543
+ );
544
+ return data.projectCreate;
545
+ };
546
+
547
+ const updateProject = async ({ projectId, ...input } = {}) => {
548
+ requireAccountToken("updateRailwayProject");
549
+ const id = await requireResolvedProjectId(projectId, "updateRailwayProject");
550
+ const data = await request(
551
+ `
552
+ mutation RailwayProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
553
+ projectUpdate(id: $id, input: $input) {
554
+ id
555
+ name
556
+ description
557
+ prDeploys
558
+ focusedPrEnvironments
559
+ botPrEnvironments
560
+ isPublic
561
+ }
562
+ }
563
+ `,
564
+ { id, input: compactObject(input) },
565
+ );
566
+ return data.projectUpdate;
567
+ };
568
+
569
+ const deleteProject = async (projectId) => {
570
+ requireAccountToken("deleteRailwayProject");
571
+ const id = await requireResolvedProjectId(projectId, "deleteRailwayProject");
572
+ const deleted = await request(
573
+ `
574
+ mutation RailwayProjectDelete($id: String!) {
575
+ projectDelete(id: $id)
576
+ }
577
+ `,
578
+ { id },
579
+ );
580
+ return {
581
+ deleted: Boolean(deleted.projectDelete),
582
+ projectId: id,
583
+ };
584
+ };
585
+
586
+ const transferProject = async ({ projectId, workspaceId } = {}) => {
587
+ requireAccountToken("transferRailwayProject");
588
+ const id = await requireResolvedProjectId(projectId, "transferRailwayProject");
589
+ const transferred = await request(
590
+ `
591
+ mutation RailwayProjectTransfer($projectId: String!, $input: ProjectTransferInput!) {
592
+ projectTransfer(projectId: $projectId, input: $input)
593
+ }
594
+ `,
595
+ {
596
+ projectId: id,
597
+ input: { workspaceId },
598
+ },
599
+ );
600
+ return {
601
+ transferred: Boolean(transferred.projectTransfer),
602
+ projectId: id,
603
+ workspaceId,
604
+ };
605
+ };
606
+
607
+ const createService = async (input = {}) => {
608
+ const data = await request(
609
+ `
610
+ mutation RailwayServiceCreate($input: ServiceCreateInput!) {
611
+ serviceCreate(input: $input) {
612
+ id
613
+ name
614
+ icon
615
+ projectId
616
+ }
617
+ }
618
+ `,
619
+ { input: compactObject(input) },
620
+ );
621
+ return data.serviceCreate;
622
+ };
623
+
624
+ const updateService = async ({ serviceId, ...input } = {}) => {
625
+ const data = await request(
626
+ `
627
+ mutation RailwayServiceUpdate($id: String!, $input: ServiceUpdateInput!) {
628
+ serviceUpdate(id: $id, input: $input) {
629
+ id
630
+ name
631
+ icon
632
+ projectId
633
+ }
634
+ }
635
+ `,
636
+ { id: serviceId, input: compactObject(input) },
637
+ );
638
+ return data.serviceUpdate;
639
+ };
640
+
641
+ const connectService = async ({ serviceId, ...input } = {}) => {
642
+ const data = await request(
643
+ `
644
+ mutation RailwayServiceConnect($id: String!, $input: ServiceConnectInput!) {
645
+ serviceConnect(id: $id, input: $input) {
646
+ id
647
+ name
648
+ icon
649
+ projectId
650
+ }
651
+ }
652
+ `,
653
+ { id: serviceId, input: compactObject(input) },
654
+ );
655
+ return data.serviceConnect;
656
+ };
657
+
658
+ const disconnectService = async (serviceId) => {
659
+ const disconnected = await request(
660
+ `
661
+ mutation RailwayServiceDisconnect($id: String!) {
662
+ serviceDisconnect(id: $id)
663
+ }
664
+ `,
665
+ { id: serviceId },
666
+ );
667
+ return {
668
+ disconnected: Boolean(disconnected.serviceDisconnect),
669
+ serviceId,
670
+ };
671
+ };
672
+
673
+ const deleteService = async ({ serviceId, environmentId } = {}) => {
674
+ const deleted = await request(
675
+ `
676
+ mutation RailwayServiceDelete($id: String!, $environmentId: String) {
677
+ serviceDelete(id: $id, environmentId: $environmentId)
678
+ }
679
+ `,
680
+ { id: serviceId, environmentId },
681
+ );
682
+ return {
683
+ deleted: Boolean(deleted.serviceDelete),
684
+ serviceId,
685
+ environmentId: environmentId ?? null,
686
+ };
687
+ };
688
+
689
+ const updateServiceInstance = async ({
690
+ serviceId,
691
+ environmentId,
692
+ ...input
693
+ } = {}) => {
694
+ const updated = await request(
695
+ `
696
+ mutation RailwayServiceInstanceUpdate(
697
+ $serviceId: String!
698
+ $environmentId: String
699
+ $input: ServiceInstanceUpdateInput!
700
+ ) {
701
+ serviceInstanceUpdate(
702
+ serviceId: $serviceId
703
+ environmentId: $environmentId
704
+ input: $input
705
+ )
706
+ }
707
+ `,
708
+ {
709
+ serviceId,
710
+ environmentId,
711
+ input: compactObject(input),
712
+ },
713
+ );
714
+ return {
715
+ updated: Boolean(updated.serviceInstanceUpdate),
716
+ serviceId,
717
+ environmentId: environmentId ?? null,
718
+ };
719
+ };
720
+
721
+ const deployService = async ({
722
+ serviceId,
723
+ environmentId,
724
+ commitSha,
725
+ latestCommit,
726
+ } = {}) => {
727
+ const deployment = await request(
728
+ `
729
+ mutation RailwayServiceInstanceDeploy(
730
+ $serviceId: String!
731
+ $environmentId: String!
732
+ $commitSha: String
733
+ $latestCommit: Boolean
734
+ ) {
735
+ serviceInstanceDeploy(
736
+ serviceId: $serviceId
737
+ environmentId: $environmentId
738
+ commitSha: $commitSha
739
+ latestCommit: $latestCommit
740
+ ) {
741
+ id
742
+ status
743
+ environmentId
744
+ serviceId
745
+ url
746
+ staticUrl
747
+ }
748
+ }
749
+ `,
750
+ { serviceId, environmentId, commitSha, latestCommit },
751
+ );
752
+ return deployment.serviceInstanceDeploy;
753
+ };
754
+
755
+ const redeployService = async ({ serviceId, environmentId } = {}) => {
756
+ const deployment = await request(
757
+ `
758
+ mutation RailwayServiceInstanceRedeploy(
759
+ $serviceId: String!
760
+ $environmentId: String!
761
+ ) {
762
+ serviceInstanceRedeploy(
763
+ serviceId: $serviceId
764
+ environmentId: $environmentId
765
+ ) {
766
+ id
767
+ status
768
+ environmentId
769
+ serviceId
770
+ url
771
+ staticUrl
772
+ }
773
+ }
774
+ `,
775
+ { serviceId, environmentId },
776
+ );
777
+ return deployment.serviceInstanceRedeploy;
778
+ };
779
+
780
+ const updateServiceInstanceLimits = async ({
781
+ serviceId,
782
+ environmentId,
783
+ memoryGB,
784
+ vCPUs,
785
+ } = {}) => {
786
+ const updated = await request(
787
+ `
788
+ mutation RailwayServiceInstanceLimitsUpdate(
789
+ $input: ServiceInstanceLimitsUpdateInput!
790
+ ) {
791
+ serviceInstanceLimitsUpdate(input: $input)
792
+ }
793
+ `,
794
+ {
795
+ input: compactObject({
796
+ serviceId,
797
+ environmentId,
798
+ memoryGB,
799
+ vCPUs,
800
+ }),
801
+ },
802
+ );
803
+ return {
804
+ updated: Boolean(updated.serviceInstanceLimitsUpdate),
805
+ serviceId,
806
+ environmentId,
807
+ memoryGB: memoryGB ?? null,
808
+ vCPUs: vCPUs ?? null,
809
+ };
810
+ };
811
+
812
+ const createEnvironment = async (input = {}) => {
813
+ const environment = await request(
814
+ `
815
+ mutation RailwayEnvironmentCreate($input: EnvironmentCreateInput!) {
816
+ environmentCreate(input: $input) {
817
+ id
818
+ name
819
+ isEphemeral
820
+ projectId
821
+ }
822
+ }
823
+ `,
824
+ { input: compactObject(input) },
825
+ );
826
+ return environment.environmentCreate;
827
+ };
828
+
829
+ const deleteEnvironment = async (environmentId) => {
830
+ const deleted = await request(
831
+ `
832
+ mutation RailwayEnvironmentDelete($id: String!) {
833
+ environmentDelete(id: $id)
834
+ }
835
+ `,
836
+ { id: environmentId },
837
+ );
838
+ return {
839
+ deleted: Boolean(deleted.environmentDelete),
840
+ environmentId,
841
+ };
842
+ };
843
+
844
+ const upsertVariable = async (input = {}) => {
845
+ const updated = await request(
846
+ `
847
+ mutation RailwayVariableUpsert($input: VariableUpsertInput!) {
848
+ variableUpsert(input: $input)
849
+ }
850
+ `,
851
+ { input: compactObject(input) },
852
+ );
853
+ return {
854
+ updated: Boolean(updated.variableUpsert),
855
+ name: input.name ?? null,
856
+ environmentId: input.environmentId ?? null,
857
+ serviceId: input.serviceId ?? null,
858
+ projectId: input.projectId ?? null,
859
+ };
860
+ };
861
+
862
+ const deleteVariable = async (input = {}) => {
863
+ const deleted = await request(
864
+ `
865
+ mutation RailwayVariableDelete($input: VariableDeleteInput!) {
866
+ variableDelete(input: $input)
867
+ }
868
+ `,
869
+ { input: compactObject(input) },
870
+ );
871
+ return {
872
+ deleted: Boolean(deleted.variableDelete),
873
+ name: input.name ?? null,
874
+ environmentId: input.environmentId ?? null,
875
+ serviceId: input.serviceId ?? null,
876
+ projectId: input.projectId ?? null,
877
+ };
878
+ };
879
+
880
+ const createServiceDomain = async (input = {}) => {
881
+ const domain = await request(
882
+ `
883
+ mutation RailwayServiceDomainCreate($input: ServiceDomainCreateInput!) {
884
+ serviceDomainCreate(input: $input) {
885
+ id
886
+ domain
887
+ }
888
+ }
889
+ `,
890
+ { input: compactObject(input) },
891
+ );
892
+ return domain.serviceDomainCreate;
893
+ };
894
+
895
+ const updateServiceDomain = async (input = {}) => {
896
+ const updated = await request(
897
+ `
898
+ mutation RailwayServiceDomainUpdate($input: ServiceDomainUpdateInput!) {
899
+ serviceDomainUpdate(input: $input)
900
+ }
901
+ `,
902
+ { input: compactObject(input) },
903
+ );
904
+ return {
905
+ updated: Boolean(updated.serviceDomainUpdate),
906
+ serviceDomainId: input.serviceDomainId ?? null,
907
+ };
908
+ };
909
+
910
+ const deleteServiceDomain = async (serviceDomainId) => {
911
+ const deleted = await request(
912
+ `
913
+ mutation RailwayServiceDomainDelete($id: String!) {
914
+ serviceDomainDelete(id: $id)
915
+ }
916
+ `,
917
+ { id: serviceDomainId },
918
+ );
919
+ return {
920
+ deleted: Boolean(deleted.serviceDomainDelete),
921
+ serviceDomainId,
922
+ };
923
+ };
924
+
925
+ const createCustomDomain = async (input = {}) => {
926
+ const domain = await request(
927
+ `
928
+ mutation RailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
929
+ customDomainCreate(input: $input) {
930
+ id
931
+ domain
932
+ }
933
+ }
934
+ `,
935
+ { input: compactObject(input) },
936
+ );
937
+ return domain.customDomainCreate;
938
+ };
939
+
940
+ const updateCustomDomain = async ({
941
+ customDomainId,
942
+ environmentId,
943
+ targetPort,
944
+ } = {}) => {
945
+ const updated = await request(
946
+ `
947
+ mutation RailwayCustomDomainUpdate(
948
+ $id: String!
949
+ $environmentId: String!
950
+ $targetPort: Int
951
+ ) {
952
+ customDomainUpdate(
953
+ id: $id
954
+ environmentId: $environmentId
955
+ targetPort: $targetPort
956
+ )
957
+ }
958
+ `,
959
+ { id: customDomainId, environmentId, targetPort },
960
+ );
961
+ return {
962
+ updated: Boolean(updated.customDomainUpdate),
963
+ customDomainId,
964
+ };
965
+ };
966
+
967
+ const deleteCustomDomain = async (customDomainId) => {
968
+ const deleted = await request(
969
+ `
970
+ mutation RailwayCustomDomainDelete($id: String!) {
971
+ customDomainDelete(id: $id)
972
+ }
973
+ `,
974
+ { id: customDomainId },
975
+ );
976
+ return {
977
+ deleted: Boolean(deleted.customDomainDelete),
978
+ customDomainId,
979
+ };
980
+ };
981
+
982
+ const createVolume = async (input = {}) => {
983
+ const volume = await request(
984
+ `
985
+ mutation RailwayVolumeCreate($input: VolumeCreateInput!) {
986
+ volumeCreate(input: $input) {
987
+ id
988
+ }
989
+ }
990
+ `,
991
+ { input: compactObject(input) },
992
+ );
993
+ return volume.volumeCreate;
994
+ };
995
+
996
+ const deleteVolume = async (volumeId) => {
997
+ const deleted = await request(
998
+ `
999
+ mutation RailwayVolumeDelete($volumeId: String!) {
1000
+ volumeDelete(volumeId: $volumeId)
1001
+ }
1002
+ `,
1003
+ { volumeId },
1004
+ );
1005
+ return {
1006
+ deleted: Boolean(deleted.volumeDelete),
1007
+ volumeId,
1008
+ };
1009
+ };
1010
+
311
1011
  const doctorProject = async ({ projectId } = {}) => {
312
1012
  const project = await getProject(projectId);
313
1013
  const primaryEnvironmentId =
@@ -364,10 +1064,41 @@ export const createRailwayClient = ({
364
1064
  getCurrentViewer,
365
1065
  validateAccountToken,
366
1066
  listProjects,
1067
+ createProject,
1068
+ updateProject,
1069
+ deleteProject,
1070
+ transferProject,
1071
+ getProjectMembers,
367
1072
  getProjectTokenContext,
368
1073
  getProject,
369
1074
  listEnvironments,
370
1075
  getEnvironment,
1076
+ createEnvironment,
1077
+ deleteEnvironment,
1078
+ getService,
1079
+ getServiceInstance,
1080
+ getServiceInstanceLimits,
1081
+ createService,
1082
+ updateService,
1083
+ connectService,
1084
+ disconnectService,
1085
+ deleteService,
1086
+ updateServiceInstance,
1087
+ deployService,
1088
+ redeployService,
1089
+ updateServiceInstanceLimits,
1090
+ getDeployment,
1091
+ listDeployments,
1092
+ upsertVariable,
1093
+ deleteVariable,
1094
+ createServiceDomain,
1095
+ updateServiceDomain,
1096
+ deleteServiceDomain,
1097
+ createCustomDomain,
1098
+ updateCustomDomain,
1099
+ deleteCustomDomain,
1100
+ createVolume,
1101
+ deleteVolume,
371
1102
  doctorProject,
372
1103
  };
373
1104
  };
@@ -382,6 +1113,11 @@ const connectionNodes = (connection) => {
382
1113
  return nodes;
383
1114
  };
384
1115
 
1116
+ const compactObject = (value) =>
1117
+ Object.fromEntries(
1118
+ Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined),
1119
+ );
1120
+
385
1121
  const hasErrors = (payload) =>
386
1122
  typeof payload === "object" &&
387
1123
  payload !== null &&
@@ -24,7 +24,8 @@ Optional environment variables:
24
24
  RAILWAY_PROJECT_ID
25
25
  RAILWAY_API_ENDPOINT
26
26
 
27
- Project tokens can inspect a single project. Account tokens can inspect account identity and list projects.
27
+ Project tokens can manage resources scoped to the attached project environment.
28
+ Account and workspace tokens can inspect and manage broader Railway resources.
28
29
 
29
30
  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
  `;
@@ -47,7 +48,7 @@ const createServer = () => {
47
48
  },
48
49
  {
49
50
  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
+ "Use these tools to inspect and manage Railway projects, environments, services, domains, variables, deployments, and volumes through the documented public API. Prefer read tools first, then use targeted write tools. Delete tools are destructive and should be used deliberately.",
51
52
  },
52
53
  );
53
54
 
@@ -156,6 +157,21 @@ const createServer = () => {
156
157
  ),
157
158
  );
158
159
 
160
+ server.registerTool(
161
+ "listRailwayProjectMembers",
162
+ {
163
+ description:
164
+ "List members of a Railway project. Requires an account or workspace token.",
165
+ inputSchema: {
166
+ projectId: z.string().min(1).optional(),
167
+ },
168
+ },
169
+ async ({ projectId } = {}) =>
170
+ createToolResult(
171
+ await withClient((client) => client.getProjectMembers(projectId)),
172
+ ),
173
+ );
174
+
159
175
  server.registerTool(
160
176
  "inspectRailwayProjectToken",
161
177
  {
@@ -183,6 +199,74 @@ const createServer = () => {
183
199
  ),
184
200
  );
185
201
 
202
+ server.registerTool(
203
+ "createRailwayProject",
204
+ {
205
+ description:
206
+ "Create a Railway project. Requires an account or workspace token.",
207
+ inputSchema: {
208
+ name: z.string().min(1).optional(),
209
+ description: z.string().optional(),
210
+ workspaceId: z.string().min(1).optional(),
211
+ defaultEnvironmentName: z.string().min(1).optional(),
212
+ isMonorepo: z.boolean().optional(),
213
+ isPublic: z.boolean().optional(),
214
+ prDeploys: z.boolean().optional(),
215
+ runtime: z.string().min(1).optional(),
216
+ repo: z.unknown().optional(),
217
+ },
218
+ },
219
+ async (args = {}) =>
220
+ createToolResult(await withClient((client) => client.createProject(args))),
221
+ );
222
+
223
+ server.registerTool(
224
+ "updateRailwayProject",
225
+ {
226
+ description:
227
+ "Update Railway project settings such as name, description, or PR environment behavior.",
228
+ inputSchema: {
229
+ projectId: z.string().min(1).optional(),
230
+ name: z.string().min(1).optional(),
231
+ description: z.string().optional(),
232
+ baseEnvironmentId: z.string().min(1).optional(),
233
+ botPrEnvironments: z.boolean().optional(),
234
+ focusedPrEnvironments: z.boolean().optional(),
235
+ isPublic: z.boolean().optional(),
236
+ prDeploys: z.boolean().optional(),
237
+ },
238
+ },
239
+ async (args = {}) =>
240
+ createToolResult(await withClient((client) => client.updateProject(args))),
241
+ );
242
+
243
+ server.registerTool(
244
+ "deleteRailwayProject",
245
+ {
246
+ description:
247
+ "Delete a Railway project. Destructive. Requires an account or workspace token.",
248
+ inputSchema: {
249
+ projectId: z.string().min(1).optional(),
250
+ },
251
+ },
252
+ async ({ projectId } = {}) =>
253
+ createToolResult(await withClient((client) => client.deleteProject(projectId))),
254
+ );
255
+
256
+ server.registerTool(
257
+ "transferRailwayProject",
258
+ {
259
+ description:
260
+ "Transfer a Railway project to another workspace. Requires an account or workspace token.",
261
+ inputSchema: {
262
+ projectId: z.string().min(1).optional(),
263
+ workspaceId: z.string().min(1),
264
+ },
265
+ },
266
+ async (args) =>
267
+ createToolResult(await withClient((client) => client.transferProject(args))),
268
+ );
269
+
186
270
  server.registerTool(
187
271
  "listRailwayEnvironments",
188
272
  {
@@ -214,6 +298,452 @@ const createServer = () => {
214
298
  ),
215
299
  );
216
300
 
301
+ server.registerTool(
302
+ "createRailwayEnvironment",
303
+ {
304
+ description:
305
+ "Create a Railway environment inside a project.",
306
+ inputSchema: {
307
+ projectId: z.string().min(1),
308
+ name: z.string().min(1),
309
+ ephemeral: z.boolean().optional(),
310
+ sourceEnvironmentId: z.string().min(1).optional(),
311
+ skipInitialDeploys: z.boolean().optional(),
312
+ stageInitialChanges: z.boolean().optional(),
313
+ applyChangesInBackground: z.boolean().optional(),
314
+ },
315
+ },
316
+ async (args) =>
317
+ createToolResult(await withClient((client) => client.createEnvironment(args))),
318
+ );
319
+
320
+ server.registerTool(
321
+ "deleteRailwayEnvironment",
322
+ {
323
+ description:
324
+ "Delete a Railway environment. Destructive.",
325
+ inputSchema: {
326
+ environmentId: z.string().min(1),
327
+ },
328
+ },
329
+ async ({ environmentId }) =>
330
+ createToolResult(
331
+ await withClient((client) => client.deleteEnvironment(environmentId)),
332
+ ),
333
+ );
334
+
335
+ server.registerTool(
336
+ "getRailwayService",
337
+ {
338
+ description: "Inspect one Railway service.",
339
+ inputSchema: {
340
+ serviceId: z.string().min(1),
341
+ },
342
+ },
343
+ async ({ serviceId }) =>
344
+ createToolResult(await withClient((client) => client.getService(serviceId))),
345
+ );
346
+
347
+ server.registerTool(
348
+ "getRailwayServiceInstance",
349
+ {
350
+ description:
351
+ "Inspect one Railway service instance in a specific environment.",
352
+ inputSchema: {
353
+ serviceId: z.string().min(1),
354
+ environmentId: z.string().min(1),
355
+ },
356
+ },
357
+ async (args) =>
358
+ createToolResult(
359
+ await withClient((client) => client.getServiceInstance(args)),
360
+ ),
361
+ );
362
+
363
+ server.registerTool(
364
+ "getRailwayServiceInstanceLimits",
365
+ {
366
+ description:
367
+ "Get resource limits for a Railway service instance.",
368
+ inputSchema: {
369
+ serviceId: z.string().min(1),
370
+ environmentId: z.string().min(1),
371
+ },
372
+ },
373
+ async (args) =>
374
+ createToolResult(
375
+ await withClient((client) => client.getServiceInstanceLimits(args)),
376
+ ),
377
+ );
378
+
379
+ server.registerTool(
380
+ "createRailwayService",
381
+ {
382
+ description:
383
+ "Create a Railway service from a repo, image, template, or as an empty service.",
384
+ inputSchema: {
385
+ projectId: z.string().min(1),
386
+ environmentId: z.string().min(1).optional(),
387
+ name: z.string().min(1).optional(),
388
+ icon: z.string().min(1).optional(),
389
+ branch: z.string().min(1).optional(),
390
+ templateId: z.string().min(1).optional(),
391
+ templateServiceId: z.string().min(1).optional(),
392
+ source: z.unknown().optional(),
393
+ registryCredentials: z.unknown().optional(),
394
+ variables: z.unknown().optional(),
395
+ },
396
+ },
397
+ async (args) =>
398
+ createToolResult(await withClient((client) => client.createService(args))),
399
+ );
400
+
401
+ server.registerTool(
402
+ "updateRailwayService",
403
+ {
404
+ description:
405
+ "Update a Railway service name or icon.",
406
+ inputSchema: {
407
+ serviceId: z.string().min(1),
408
+ name: z.string().min(1).optional(),
409
+ icon: z.string().min(1).optional(),
410
+ },
411
+ },
412
+ async (args) =>
413
+ createToolResult(await withClient((client) => client.updateService(args))),
414
+ );
415
+
416
+ server.registerTool(
417
+ "connectRailwayService",
418
+ {
419
+ description:
420
+ "Connect an existing Railway service to a repo or image source.",
421
+ inputSchema: {
422
+ serviceId: z.string().min(1),
423
+ repo: z.string().min(1).optional(),
424
+ image: z.string().min(1).optional(),
425
+ branch: z.string().min(1).optional(),
426
+ },
427
+ },
428
+ async (args) =>
429
+ createToolResult(await withClient((client) => client.connectService(args))),
430
+ );
431
+
432
+ server.registerTool(
433
+ "disconnectRailwayService",
434
+ {
435
+ description:
436
+ "Disconnect a Railway service from its current source.",
437
+ inputSchema: {
438
+ serviceId: z.string().min(1),
439
+ },
440
+ },
441
+ async ({ serviceId }) =>
442
+ createToolResult(
443
+ await withClient((client) => client.disconnectService(serviceId)),
444
+ ),
445
+ );
446
+
447
+ server.registerTool(
448
+ "deleteRailwayService",
449
+ {
450
+ description:
451
+ "Delete a Railway service. Destructive.",
452
+ inputSchema: {
453
+ serviceId: z.string().min(1),
454
+ environmentId: z.string().min(1).optional(),
455
+ },
456
+ },
457
+ async (args) =>
458
+ createToolResult(await withClient((client) => client.deleteService(args))),
459
+ );
460
+
461
+ server.registerTool(
462
+ "updateRailwayServiceInstance",
463
+ {
464
+ description:
465
+ "Update build, deploy, region, healthcheck, cron, or source settings for a Railway service instance.",
466
+ inputSchema: {
467
+ serviceId: z.string().min(1),
468
+ environmentId: z.string().min(1).optional(),
469
+ buildCommand: z.string().optional(),
470
+ builder: z.string().min(1).optional(),
471
+ cronSchedule: z.string().optional(),
472
+ dockerfilePath: z.string().optional(),
473
+ drainingSeconds: z.number().int().min(0).optional(),
474
+ healthcheckPath: z.string().optional(),
475
+ healthcheckTimeout: z.number().int().min(0).optional(),
476
+ ipv6EgressEnabled: z.boolean().optional(),
477
+ multiRegionConfig: z.unknown().optional(),
478
+ nixpacksPlan: z.unknown().optional(),
479
+ numReplicas: z.number().int().min(0).optional(),
480
+ overlapSeconds: z.number().int().min(0).optional(),
481
+ preDeployCommand: z.array(z.string()).optional(),
482
+ railwayConfigFile: z.string().optional(),
483
+ region: z.string().optional(),
484
+ registryCredentials: z.unknown().optional(),
485
+ restartPolicyMaxRetries: z.number().int().min(0).optional(),
486
+ restartPolicyType: z.string().min(1).optional(),
487
+ rootDirectory: z.string().optional(),
488
+ sleepApplication: z.boolean().optional(),
489
+ source: z.unknown().optional(),
490
+ startCommand: z.string().optional(),
491
+ watchPatterns: z.array(z.string()).optional(),
492
+ },
493
+ },
494
+ async (args) =>
495
+ createToolResult(
496
+ await withClient((client) => client.updateServiceInstance(args)),
497
+ ),
498
+ );
499
+
500
+ server.registerTool(
501
+ "deployRailwayService",
502
+ {
503
+ description:
504
+ "Trigger a Railway deployment for a service instance.",
505
+ inputSchema: {
506
+ serviceId: z.string().min(1),
507
+ environmentId: z.string().min(1),
508
+ commitSha: z.string().min(1).optional(),
509
+ latestCommit: z.boolean().optional(),
510
+ },
511
+ },
512
+ async (args) =>
513
+ createToolResult(await withClient((client) => client.deployService(args))),
514
+ );
515
+
516
+ server.registerTool(
517
+ "redeployRailwayService",
518
+ {
519
+ description:
520
+ "Redeploy the latest Railway deployment for a service instance.",
521
+ inputSchema: {
522
+ serviceId: z.string().min(1),
523
+ environmentId: z.string().min(1),
524
+ },
525
+ },
526
+ async (args) =>
527
+ createToolResult(await withClient((client) => client.redeployService(args))),
528
+ );
529
+
530
+ server.registerTool(
531
+ "updateRailwayServiceInstanceLimits",
532
+ {
533
+ description:
534
+ "Update vCPU or memory limits for a Railway service instance.",
535
+ inputSchema: {
536
+ serviceId: z.string().min(1),
537
+ environmentId: z.string().min(1),
538
+ memoryGB: z.number().positive().optional(),
539
+ vCPUs: z.number().positive().optional(),
540
+ },
541
+ },
542
+ async (args) =>
543
+ createToolResult(
544
+ await withClient((client) => client.updateServiceInstanceLimits(args)),
545
+ ),
546
+ );
547
+
548
+ server.registerTool(
549
+ "getRailwayDeployment",
550
+ {
551
+ description: "Inspect one Railway deployment.",
552
+ inputSchema: {
553
+ deploymentId: z.string().min(1),
554
+ },
555
+ },
556
+ async ({ deploymentId }) =>
557
+ createToolResult(
558
+ await withClient((client) => client.getDeployment(deploymentId)),
559
+ ),
560
+ );
561
+
562
+ server.registerTool(
563
+ "listRailwayDeployments",
564
+ {
565
+ description:
566
+ "List Railway deployments for a project, environment, or service.",
567
+ inputSchema: {
568
+ projectId: z.string().min(1).optional(),
569
+ environmentId: z.string().min(1).optional(),
570
+ serviceId: z.string().min(1).optional(),
571
+ first: z.number().int().min(1).max(100).optional(),
572
+ after: z.string().min(1).optional(),
573
+ before: z.string().min(1).optional(),
574
+ last: z.number().int().min(1).max(100).optional(),
575
+ },
576
+ },
577
+ async (args = {}) =>
578
+ createToolResult(await withClient((client) => client.listDeployments(args))),
579
+ );
580
+
581
+ server.registerTool(
582
+ "upsertRailwayVariable",
583
+ {
584
+ description:
585
+ "Create or update a Railway variable.",
586
+ inputSchema: {
587
+ projectId: z.string().min(1),
588
+ environmentId: z.string().min(1),
589
+ name: z.string().min(1),
590
+ value: z.string(),
591
+ serviceId: z.string().min(1).optional(),
592
+ skipDeploys: z.boolean().optional(),
593
+ },
594
+ },
595
+ async (args) =>
596
+ createToolResult(await withClient((client) => client.upsertVariable(args))),
597
+ );
598
+
599
+ server.registerTool(
600
+ "deleteRailwayVariable",
601
+ {
602
+ description:
603
+ "Delete a Railway variable.",
604
+ inputSchema: {
605
+ projectId: z.string().min(1),
606
+ environmentId: z.string().min(1),
607
+ name: z.string().min(1),
608
+ serviceId: z.string().min(1).optional(),
609
+ },
610
+ },
611
+ async (args) =>
612
+ createToolResult(await withClient((client) => client.deleteVariable(args))),
613
+ );
614
+
615
+ server.registerTool(
616
+ "createRailwayServiceDomain",
617
+ {
618
+ description:
619
+ "Create a Railway-managed service domain.",
620
+ inputSchema: {
621
+ serviceId: z.string().min(1),
622
+ environmentId: z.string().min(1),
623
+ targetPort: z.number().int().min(1).optional(),
624
+ },
625
+ },
626
+ async (args) =>
627
+ createToolResult(
628
+ await withClient((client) => client.createServiceDomain(args)),
629
+ ),
630
+ );
631
+
632
+ server.registerTool(
633
+ "updateRailwayServiceDomain",
634
+ {
635
+ description:
636
+ "Update a Railway-managed service domain target port or domain binding.",
637
+ inputSchema: {
638
+ serviceDomainId: z.string().min(1),
639
+ serviceId: z.string().min(1),
640
+ environmentId: z.string().min(1),
641
+ domain: z.string().min(1),
642
+ targetPort: z.number().int().min(1).optional(),
643
+ },
644
+ },
645
+ async (args) =>
646
+ createToolResult(
647
+ await withClient((client) => client.updateServiceDomain(args)),
648
+ ),
649
+ );
650
+
651
+ server.registerTool(
652
+ "deleteRailwayServiceDomain",
653
+ {
654
+ description:
655
+ "Delete a Railway-managed service domain.",
656
+ inputSchema: {
657
+ serviceDomainId: z.string().min(1),
658
+ },
659
+ },
660
+ async ({ serviceDomainId }) =>
661
+ createToolResult(
662
+ await withClient((client) => client.deleteServiceDomain(serviceDomainId)),
663
+ ),
664
+ );
665
+
666
+ server.registerTool(
667
+ "createRailwayCustomDomain",
668
+ {
669
+ description:
670
+ "Add a custom domain to a Railway service.",
671
+ inputSchema: {
672
+ projectId: z.string().min(1),
673
+ environmentId: z.string().min(1),
674
+ serviceId: z.string().min(1),
675
+ domain: z.string().min(1),
676
+ targetPort: z.number().int().min(1).optional(),
677
+ },
678
+ },
679
+ async (args) =>
680
+ createToolResult(
681
+ await withClient((client) => client.createCustomDomain(args)),
682
+ ),
683
+ );
684
+
685
+ server.registerTool(
686
+ "updateRailwayCustomDomain",
687
+ {
688
+ description:
689
+ "Update a custom Railway domain target port.",
690
+ inputSchema: {
691
+ customDomainId: z.string().min(1),
692
+ environmentId: z.string().min(1),
693
+ targetPort: z.number().int().min(1).optional(),
694
+ },
695
+ },
696
+ async (args) =>
697
+ createToolResult(
698
+ await withClient((client) => client.updateCustomDomain(args)),
699
+ ),
700
+ );
701
+
702
+ server.registerTool(
703
+ "deleteRailwayCustomDomain",
704
+ {
705
+ description:
706
+ "Delete a custom Railway domain.",
707
+ inputSchema: {
708
+ customDomainId: z.string().min(1),
709
+ },
710
+ },
711
+ async ({ customDomainId }) =>
712
+ createToolResult(
713
+ await withClient((client) => client.deleteCustomDomain(customDomainId)),
714
+ ),
715
+ );
716
+
717
+ server.registerTool(
718
+ "createRailwayVolume",
719
+ {
720
+ description:
721
+ "Create a Railway volume.",
722
+ inputSchema: {
723
+ projectId: z.string().min(1),
724
+ mountPath: z.string().min(1),
725
+ environmentId: z.string().min(1).optional(),
726
+ serviceId: z.string().min(1).optional(),
727
+ region: z.string().min(1).optional(),
728
+ },
729
+ },
730
+ async (args) =>
731
+ createToolResult(await withClient((client) => client.createVolume(args))),
732
+ );
733
+
734
+ server.registerTool(
735
+ "deleteRailwayVolume",
736
+ {
737
+ description:
738
+ "Delete a Railway volume. Destructive.",
739
+ inputSchema: {
740
+ volumeId: z.string().min(1),
741
+ },
742
+ },
743
+ async ({ volumeId }) =>
744
+ createToolResult(await withClient((client) => client.deleteVolume(volumeId))),
745
+ );
746
+
217
747
  server.registerTool(
218
748
  "doctorRailwayProject",
219
749
  {