@akira-tl/forgerelay 0.3.2 → 0.3.3
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/CHANGELOG.md +11 -0
- package/capabilities/artifacts-review/GUIDE.md +9 -7
- package/dist/artifact-tools.js +3 -2
- package/dist/capability-registry.js +63 -5
- package/dist/server.js +101 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.3] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `review.changes` and `artifact.download` as registered Capability Gateway actions. Review preserves the existing Git-backed checkpoint and diff-card metadata; native artifact ingress preserves Host-native file transport, workspace-relative no-overwrite publication, size limits, and `AfterFileChange` lifecycle reporting.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The Capability catalog now advertises only capabilities that are actually available in the current runtime. Explicit calls to known-but-disabled capabilities still return stable `capability_unavailable` diagnostics.
|
|
16
|
+
- `show_changes` and `download_artifact` remain compatibility aliases for the 0.3.3 migration window, but the artifacts/review guide now treats the Capability Gateway as the canonical Agent workflow.
|
|
17
|
+
|
|
7
18
|
## [0.3.2] - 2026-08-10
|
|
8
19
|
|
|
9
20
|
### Added
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## Native artifact transfer
|
|
6
6
|
|
|
7
|
-
Native artifact download
|
|
7
|
+
Native artifact download 默认关闭,且当前安全发布实现主要面向 Linux。只有 `open_workspace` 的 Capability catalog 广告 `artifact.download` 时才执行它;不熟悉 contract 时先 `capability(action="describe")`,其中会明确 native file 通过 Gateway 顶层 `file` transport slot 传入。
|
|
8
8
|
|
|
9
9
|
典型流程:
|
|
10
10
|
|
|
11
11
|
1. 先用 `open_workspace` 打开目标项目;
|
|
12
|
-
2. 调用 `
|
|
12
|
+
2. 调用 `capability`,`name="artifact.download"`、`action="run"`,把 Host 原样提供的 native `file` 值放在 Gateway 顶层 `file`,并在 `arguments.path` 传 workspace-relative 目标路径;
|
|
13
13
|
3. 成功后使用返回的 normalized relative path 配合普通 `read` / `rename` / `delete` / shell 等工具继续处理。
|
|
14
14
|
|
|
15
|
-
`download_artifact`
|
|
15
|
+
0.3.3 仍保留 `download_artifact` 作为迁移期 compatibility alias,但新的 Agent workflow 不应把它视为长期 public surface。
|
|
16
|
+
|
|
17
|
+
`artifact.download` 会创建缺失的父目录,但拒绝覆盖已有目标。不要把 native file 值改造成:
|
|
16
18
|
|
|
17
19
|
- 任意 URL 字符串;
|
|
18
20
|
- 猜测的本地主机路径;
|
|
@@ -28,15 +30,15 @@ Native artifact download 默认关闭。只有 `tools/list` 实际暴露 `downlo
|
|
|
28
30
|
`FORGERELAY_WIDGETS`:
|
|
29
31
|
|
|
30
32
|
- `full`:默认,为常用 workspace/file/edit/shell tools 附加 MCP App UI;
|
|
31
|
-
- `changes`:聚焦 `open_workspace` + `
|
|
33
|
+
- `changes`:聚焦 `open_workspace` + `review.changes` 聚合 review;
|
|
32
34
|
- `off`:不附加 widget UI。
|
|
33
35
|
|
|
34
36
|
Plain MCP Host 可以忽略这些 UI metadata;不要把 UI 是否显示当成文件操作是否成功的判据。
|
|
35
37
|
|
|
36
|
-
当
|
|
38
|
+
当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。0.3.3 仍保留 `show_changes` compatibility alias,但它和 `review.changes` 共用同一套 checkpoint,不是两套 review 状态。
|
|
37
39
|
|
|
38
|
-
`
|
|
40
|
+
`review.changes` 使用 Git-backed review checkpoint。它按 workspace 跟踪 open/baseline 状态,展示自上次 review checkpoint 以来的 coherent diff,并在成功 review 后推进 baseline。当前版本要求可用的 Git workspace;checkpoint 缺失或 root 不匹配时会明确失败或使用受控 fallback,而不是凭空重建历史。
|
|
39
41
|
|
|
40
42
|
## Capability ownership
|
|
41
43
|
|
|
42
|
-
Artifact transfer 和 change review 都属于 ForgeRelay capability,而不是 Agent
|
|
44
|
+
Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只负责暴露稳定 Gateway 与当前兼容 aliases;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
|
package/dist/artifact-tools.js
CHANGED
|
@@ -32,8 +32,9 @@ const openAIFileReferenceInputSchema = z.strictObject({
|
|
|
32
32
|
export function isArtifactDownloadSupportedPlatform(platform = process.platform) {
|
|
33
33
|
return ARTIFACT_DOWNLOAD_PLATFORMS.has(platform);
|
|
34
34
|
}
|
|
35
|
-
export function registerArtifactTools(server, { config, workspaces, hooks, incomingArtifactAdapters = [], }) {
|
|
36
|
-
const incomingRegistry =
|
|
35
|
+
export function registerArtifactTools(server, { config, workspaces, hooks, incomingArtifactAdapters = [], incomingArtifactRegistry, }) {
|
|
36
|
+
const incomingRegistry = incomingArtifactRegistry
|
|
37
|
+
?? new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
37
38
|
registerAppTool(server, "download_artifact", {
|
|
38
39
|
title: "Download attached or generated file",
|
|
39
40
|
description: "Stream one MCP-host-provided native file to a requested relative path inside an already-open workspace. Existing destinations, arbitrary URLs, absolute paths, traversal, symlinked parents, local source paths, and malformed file objects are rejected.",
|
|
@@ -31,7 +31,7 @@ export class CapabilityRegistry {
|
|
|
31
31
|
readBeforeFirstUse: definition.readGuideBeforeFirstUse,
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
|
-
});
|
|
34
|
+
}).filter((entry) => entry.available);
|
|
35
35
|
}
|
|
36
36
|
describe(name, context) {
|
|
37
37
|
const definition = this.requireDefinition(name);
|
|
@@ -48,15 +48,29 @@ export class CapabilityRegistry {
|
|
|
48
48
|
whenToRead: guide.whenToRead,
|
|
49
49
|
},
|
|
50
50
|
inputSchema: z.toJSONSchema(definition.inputSchema, { target: "draft-7" }),
|
|
51
|
+
...(definition.nativeFileArgument
|
|
52
|
+
? {
|
|
53
|
+
transport: {
|
|
54
|
+
nativeFileArgument: definition.nativeFileArgument,
|
|
55
|
+
gatewayParameter: "file",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
|
-
async run(name, argumentsValue, context) {
|
|
61
|
+
async run(name, argumentsValue, context, options = {}) {
|
|
54
62
|
const definition = this.requireDefinition(name);
|
|
55
63
|
const catalogEntry = this.catalogEntry(definition, context);
|
|
56
64
|
if (!catalogEntry.available) {
|
|
57
65
|
throw new CapabilityError("capability_unavailable", `Capability ${name} is unavailable${catalogEntry.unavailableReason ? `: ${catalogEntry.unavailableReason}` : "."}`);
|
|
58
66
|
}
|
|
59
|
-
|
|
67
|
+
if (options.nativeFile !== undefined && !definition.nativeFileArgument) {
|
|
68
|
+
throw new CapabilityError("invalid_arguments", `Capability ${name} does not accept a Host-native file value.`);
|
|
69
|
+
}
|
|
70
|
+
const input = definition.nativeFileArgument && options.nativeFile !== undefined
|
|
71
|
+
? { ...(isRecord(argumentsValue) ? argumentsValue : {}), [definition.nativeFileArgument]: options.nativeFile }
|
|
72
|
+
: argumentsValue ?? {};
|
|
73
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
60
74
|
if (!parsed.success) {
|
|
61
75
|
const details = parsed.error.issues
|
|
62
76
|
.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "arguments"}: ${issue.message}`)
|
|
@@ -110,9 +124,53 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
110
124
|
inputSchema: hooksCheckInput,
|
|
111
125
|
availability: () => ({ available: true }),
|
|
112
126
|
run: async (_input, context) => ({
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
value: {
|
|
128
|
+
ok: true,
|
|
129
|
+
...await dependencies.inspectHooks(context.workspaceRoot),
|
|
130
|
+
},
|
|
115
131
|
}),
|
|
116
132
|
},
|
|
133
|
+
...(dependencies.reviewChanges
|
|
134
|
+
? [{
|
|
135
|
+
name: "review.changes",
|
|
136
|
+
description: "Review accumulated workspace changes from the Git-backed review checkpoint.",
|
|
137
|
+
guideName: "artifacts-review",
|
|
138
|
+
readGuideBeforeFirstUse: true,
|
|
139
|
+
inputSchema: z.object({}).strict(),
|
|
140
|
+
availability: () => ({
|
|
141
|
+
available: dependencies.reviewChanges?.available ?? false,
|
|
142
|
+
reason: dependencies.reviewChanges?.unavailableReason,
|
|
143
|
+
}),
|
|
144
|
+
run: async (_input, context) => dependencies.reviewChanges.run(context),
|
|
145
|
+
}]
|
|
146
|
+
: []),
|
|
147
|
+
...(dependencies.downloadArtifact
|
|
148
|
+
? [{
|
|
149
|
+
name: "artifact.download",
|
|
150
|
+
description: "Save one Host-native file into a workspace-relative destination without overwriting.",
|
|
151
|
+
guideName: "artifacts-review",
|
|
152
|
+
readGuideBeforeFirstUse: true,
|
|
153
|
+
inputSchema: z.object({
|
|
154
|
+
file: z.strictObject({
|
|
155
|
+
download_url: z.string(),
|
|
156
|
+
file_id: z.string(),
|
|
157
|
+
mime_type: z.string().nullable().optional(),
|
|
158
|
+
file_name: z.string().nullable().optional(),
|
|
159
|
+
name: z.string().nullable().optional(),
|
|
160
|
+
size: z.number().int().nonnegative().nullable().optional(),
|
|
161
|
+
}),
|
|
162
|
+
path: z.string().min(1),
|
|
163
|
+
}).strict(),
|
|
164
|
+
nativeFileArgument: "file",
|
|
165
|
+
availability: () => ({
|
|
166
|
+
available: dependencies.downloadArtifact?.available ?? false,
|
|
167
|
+
reason: dependencies.downloadArtifact?.unavailableReason,
|
|
168
|
+
}),
|
|
169
|
+
run: async (input, context) => dependencies.downloadArtifact.run(input, context),
|
|
170
|
+
}]
|
|
171
|
+
: []),
|
|
117
172
|
]);
|
|
118
173
|
}
|
|
174
|
+
function isRecord(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
176
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -17,12 +17,13 @@ import { applyPatch } from "./apply-patch.js";
|
|
|
17
17
|
import { buildCapabilityFingerprint } from "./capabilities.js";
|
|
18
18
|
import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
|
|
19
19
|
import { deletePath, renamePath } from "./file-mutations.js";
|
|
20
|
-
import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
|
|
20
|
+
import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
|
|
21
|
+
import { ArtifactError } from "./artifact-error.js";
|
|
21
22
|
import { loadConfig } from "./config.js";
|
|
22
23
|
import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
|
|
23
24
|
import { checkHookConfiguration } from "./hook-cli.js";
|
|
24
25
|
import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
|
|
25
|
-
import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
|
|
26
|
+
import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
|
|
26
27
|
import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
|
|
27
28
|
import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
|
|
28
29
|
import { SingleUserOAuthProvider } from "./oauth-provider.js";
|
|
@@ -66,7 +67,7 @@ function shouldAttachWidget(mode, kind) {
|
|
|
66
67
|
case "off":
|
|
67
68
|
return false;
|
|
68
69
|
case "changes":
|
|
69
|
-
return kind === "workspace" || kind === "show_changes";
|
|
70
|
+
return kind === "workspace" || kind === "show_changes" || kind === "capability";
|
|
70
71
|
case "full":
|
|
71
72
|
return true;
|
|
72
73
|
}
|
|
@@ -522,6 +523,13 @@ function capabilityContextFor(workspace) {
|
|
|
522
523
|
})),
|
|
523
524
|
};
|
|
524
525
|
}
|
|
526
|
+
async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
|
|
527
|
+
return reviewCheckpoints.reviewChanges({
|
|
528
|
+
workspaceId: workspace.id,
|
|
529
|
+
root: workspace.root,
|
|
530
|
+
markReviewed: true,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
525
533
|
function toolResultIsError(result) {
|
|
526
534
|
return typeof result === "object" && result !== null && result.isError === true;
|
|
527
535
|
}
|
|
@@ -679,8 +687,66 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
679
687
|
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters) {
|
|
680
688
|
const toolDescriptions = buildToolDescriptions(config);
|
|
681
689
|
const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
|
|
690
|
+
const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
691
|
+
const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
|
|
692
|
+
const reviewChangesAvailable = config.widgets === "changes";
|
|
682
693
|
const capabilityRegistry = createCapabilityRegistry({
|
|
683
694
|
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
|
|
695
|
+
reviewChanges: {
|
|
696
|
+
available: reviewChangesAvailable,
|
|
697
|
+
unavailableReason: reviewChangesAvailable
|
|
698
|
+
? undefined
|
|
699
|
+
: "Aggregate change review is disabled; start ForgeRelay with widgets=changes.",
|
|
700
|
+
run: async (context) => {
|
|
701
|
+
const review = await reviewWorkspaceChanges(reviewCheckpoints, {
|
|
702
|
+
id: context.workspaceId,
|
|
703
|
+
root: context.workspaceRoot,
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
value: {
|
|
707
|
+
result: review.result,
|
|
708
|
+
summary: review.summary,
|
|
709
|
+
files: review.files,
|
|
710
|
+
},
|
|
711
|
+
card: {
|
|
712
|
+
tool: "show_changes",
|
|
713
|
+
summary: review.summary,
|
|
714
|
+
files: review.files,
|
|
715
|
+
payload: { patch: review.patch },
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
downloadArtifact: {
|
|
721
|
+
available: artifactDownloadAvailable,
|
|
722
|
+
unavailableReason: !config.artifactsEnabled
|
|
723
|
+
? "Native artifact ingress is disabled."
|
|
724
|
+
: !isArtifactDownloadSupportedPlatform()
|
|
725
|
+
? "Native artifact ingress is unsupported on this platform."
|
|
726
|
+
: undefined,
|
|
727
|
+
run: async (input, context) => {
|
|
728
|
+
try {
|
|
729
|
+
const downloaded = await downloadIncomingArtifact({
|
|
730
|
+
registry: incomingArtifactRegistry,
|
|
731
|
+
workspaceId: context.workspaceId,
|
|
732
|
+
workspaceRoot: context.workspaceRoot,
|
|
733
|
+
maxFileBytes: config.artifactMaxFileBytes,
|
|
734
|
+
file: input.file,
|
|
735
|
+
path: input.path,
|
|
736
|
+
});
|
|
737
|
+
return {
|
|
738
|
+
value: { path: downloaded.path },
|
|
739
|
+
changedPaths: [downloaded.path],
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
if (error instanceof ArtifactError) {
|
|
744
|
+
throw new CapabilityError(`artifact.${error.code}`, error.message);
|
|
745
|
+
}
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
},
|
|
684
750
|
});
|
|
685
751
|
const server = new McpServer({
|
|
686
752
|
name: "forgerelay",
|
|
@@ -987,7 +1053,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
987
1053
|
arguments: z
|
|
988
1054
|
.record(z.string(), z.unknown())
|
|
989
1055
|
.optional()
|
|
990
|
-
.describe("Capability-specific arguments. Omit for describe and for capabilities with no arguments."),
|
|
1056
|
+
.describe("Capability-specific JSON arguments. Omit for describe and for capabilities with no arguments."),
|
|
1057
|
+
file: z
|
|
1058
|
+
.unknown()
|
|
1059
|
+
.optional()
|
|
1060
|
+
.describe("Host-native file value. Only capabilities whose describe result advertises native-file transport may consume it."),
|
|
991
1061
|
},
|
|
992
1062
|
outputSchema: {
|
|
993
1063
|
name: z.string(),
|
|
@@ -996,20 +1066,25 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
996
1066
|
result: z.unknown().optional(),
|
|
997
1067
|
error: capabilityErrorOutputSchema.optional(),
|
|
998
1068
|
},
|
|
999
|
-
_meta: {
|
|
1069
|
+
_meta: {
|
|
1070
|
+
...toolWidgetDescriptorMeta(config, "capability")._meta,
|
|
1071
|
+
"openai/fileParams": ["file"],
|
|
1072
|
+
},
|
|
1000
1073
|
annotations: {
|
|
1001
1074
|
readOnlyHint: false,
|
|
1002
1075
|
destructiveHint: false,
|
|
1003
1076
|
idempotentHint: false,
|
|
1004
|
-
openWorldHint:
|
|
1077
|
+
openWorldHint: true,
|
|
1005
1078
|
},
|
|
1006
|
-
}, async ({ workspaceId, name, action, arguments: capabilityArguments }) => {
|
|
1079
|
+
}, async ({ workspaceId, name, action, arguments: capabilityArguments, file }) => {
|
|
1007
1080
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1081
|
+
let changedPaths = [];
|
|
1008
1082
|
return runToolWithHooks(hooks, {
|
|
1009
1083
|
tool: toolNames.capability,
|
|
1010
1084
|
invocation: workspaceHookInvocation(workspace),
|
|
1011
1085
|
payload: { name, action },
|
|
1012
1086
|
isFailure: toolResultIsError,
|
|
1087
|
+
changedPaths: () => changedPaths,
|
|
1013
1088
|
operation: async () => {
|
|
1014
1089
|
const startedAt = performance.now();
|
|
1015
1090
|
try {
|
|
@@ -1036,10 +1111,24 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1036
1111
|
});
|
|
1037
1112
|
return result;
|
|
1038
1113
|
}
|
|
1039
|
-
const
|
|
1114
|
+
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), { nativeFile: file });
|
|
1115
|
+
changedPaths = execution.changedPaths ?? [];
|
|
1040
1116
|
const result = {
|
|
1041
|
-
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(
|
|
1042
|
-
|
|
1117
|
+
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
|
|
1118
|
+
...(execution.card
|
|
1119
|
+
? {
|
|
1120
|
+
_meta: {
|
|
1121
|
+
tool: execution.card.tool ?? toolNames.capability,
|
|
1122
|
+
card: {
|
|
1123
|
+
workspaceId,
|
|
1124
|
+
summary: execution.card.summary ?? {},
|
|
1125
|
+
files: execution.card.files,
|
|
1126
|
+
payload: execution.card.payload ?? {},
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
}
|
|
1130
|
+
: {}),
|
|
1131
|
+
structuredContent: { name, action, result: execution.value },
|
|
1043
1132
|
};
|
|
1044
1133
|
logToolCall(config, {
|
|
1045
1134
|
tool: toolNames.capability,
|
|
@@ -1651,11 +1740,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1651
1740
|
invocation: workspaceHookInvocation(workspace),
|
|
1652
1741
|
operation: async () => {
|
|
1653
1742
|
const startedAt = performance.now();
|
|
1654
|
-
const review = await reviewCheckpoints
|
|
1655
|
-
workspaceId,
|
|
1656
|
-
root: workspace.root,
|
|
1657
|
-
markReviewed: true,
|
|
1658
|
-
});
|
|
1743
|
+
const review = await reviewWorkspaceChanges(reviewCheckpoints, workspace);
|
|
1659
1744
|
const content = [textBlock(review.result)];
|
|
1660
1745
|
logToolCall(config, {
|
|
1661
1746
|
tool: "show_changes",
|
|
@@ -1957,6 +2042,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1957
2042
|
workspaces,
|
|
1958
2043
|
hooks,
|
|
1959
2044
|
incomingArtifactAdapters,
|
|
2045
|
+
incomingArtifactRegistry,
|
|
1960
2046
|
});
|
|
1961
2047
|
}
|
|
1962
2048
|
return server;
|