@ai-sdk/harness-opencode 1.0.10 → 1.0.11
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 +7 -0
- package/dist/bridge/host-tool-mcp.mjs +1 -3
- package/dist/bridge/host-tool-mcp.mjs.map +1 -1
- package/dist/bridge/index.mjs +167 -19
- package/dist/bridge/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/bridge/host-tool-mcp.ts +0 -2
- package/src/bridge/index.ts +80 -25
- package/src/bridge/tool-relay-auth.ts +151 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { z } from "zod/v4";
|
|
7
7
|
var schemas = JSON.parse(process.env.TOOL_SCHEMAS || "[]");
|
|
8
8
|
var relayUrl = process.env.TOOL_RELAY_URL || "";
|
|
9
|
-
var relayToken = process.env.TOOL_RELAY_TOKEN || "";
|
|
10
9
|
if (!schemas.length || !relayUrl) {
|
|
11
10
|
process.stderr.write(
|
|
12
11
|
"[host-tool-mcp] Missing TOOL_SCHEMAS or TOOL_RELAY_URL; exiting\n"
|
|
@@ -26,8 +25,7 @@ for (const schema of schemas) {
|
|
|
26
25
|
const res = await fetch(relayUrl, {
|
|
27
26
|
method: "POST",
|
|
28
27
|
headers: {
|
|
29
|
-
"Content-Type": "application/json"
|
|
30
|
-
...relayToken ? { Authorization: `Bearer ${relayToken}` } : {}
|
|
28
|
+
"Content-Type": "application/json"
|
|
31
29
|
},
|
|
32
30
|
body: JSON.stringify({ requestId, toolName: schema.name, input })
|
|
33
31
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/*\n * These bridge imports are externalized by tsup and resolved inside the\n * sandbox from src/bridge/package.json and its lockfile. Keep this file,\n * tsup.config.ts, and the bridge package dependency list in sync.\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v4';\n\ntype ToolSchema = {\n name: string;\n description?: string;\n inputSchema?: JsonSchemaObject;\n};\n\ntype JsonSchemaObject = {\n type?: string | string[];\n description?: string;\n properties?: Record<string, JsonSchemaObject>;\n required?: string[];\n items?: JsonSchemaObject;\n enum?: unknown[];\n const?: unknown;\n oneOf?: JsonSchemaObject[];\n anyOf?: JsonSchemaObject[];\n additionalProperties?: boolean | JsonSchemaObject;\n nullable?: boolean;\n};\n\ntype ZodShape = Record<string, z.ZodTypeAny>;\n\nconst schemas: ToolSchema[] = JSON.parse(process.env.TOOL_SCHEMAS || '[]');\nconst relayUrl = process.env.TOOL_RELAY_URL || '';\
|
|
1
|
+
{"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/*\n * These bridge imports are externalized by tsup and resolved inside the\n * sandbox from src/bridge/package.json and its lockfile. Keep this file,\n * tsup.config.ts, and the bridge package dependency list in sync.\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v4';\n\ntype ToolSchema = {\n name: string;\n description?: string;\n inputSchema?: JsonSchemaObject;\n};\n\ntype JsonSchemaObject = {\n type?: string | string[];\n description?: string;\n properties?: Record<string, JsonSchemaObject>;\n required?: string[];\n items?: JsonSchemaObject;\n enum?: unknown[];\n const?: unknown;\n oneOf?: JsonSchemaObject[];\n anyOf?: JsonSchemaObject[];\n additionalProperties?: boolean | JsonSchemaObject;\n nullable?: boolean;\n};\n\ntype ZodShape = Record<string, z.ZodTypeAny>;\n\nconst schemas: ToolSchema[] = JSON.parse(process.env.TOOL_SCHEMAS || '[]');\nconst relayUrl = process.env.TOOL_RELAY_URL || '';\n\nif (!schemas.length || !relayUrl) {\n process.stderr.write(\n '[host-tool-mcp] Missing TOOL_SCHEMAS or TOOL_RELAY_URL; exiting\\n',\n );\n process.exit(0);\n}\n\nconst server = new McpServer({ name: 'harness-tools', version: '1.0.0' });\n\nfor (const schema of schemas) {\n const shape = toZodShape(schema.inputSchema);\n server.tool(\n schema.name,\n schema.description ?? '',\n shape,\n async (input: Record<string, unknown>) => {\n const requestId = crypto.randomUUID();\n try {\n const res = await fetch(relayUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ requestId, toolName: schema.name, input }),\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(\n `Tool relay ${schema.name} failed with ${res.status}: ${body.slice(0, 500)}`,\n );\n }\n const data = (await res.json()) as { result?: unknown };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(data.result ?? null),\n },\n ],\n };\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],\n isError: true,\n };\n }\n },\n );\n}\n\nfunction toZodShape(schema: JsonSchemaObject | undefined): ZodShape {\n if (!schema?.properties) return {};\n const required = new Set(schema.required ?? []);\n const shape: ZodShape = {};\n for (const [key, propSchema] of Object.entries(schema.properties)) {\n const propType = toZodType(propSchema);\n shape[key] = required.has(key) ? propType : propType.optional();\n }\n return shape;\n}\n\nfunction toZodType(schema: JsonSchemaObject | undefined): z.ZodTypeAny {\n if (!schema) return z.any();\n const types = Array.isArray(schema.type)\n ? schema.type.filter((t): t is string => t !== 'null')\n : ([schema.type].filter(Boolean) as string[]);\n let zType: z.ZodTypeAny;\n switch (types[0]) {\n case 'string':\n zType = z.string();\n break;\n case 'number':\n zType = z.number();\n break;\n case 'integer':\n zType = z.number().int();\n break;\n case 'boolean':\n zType = z.boolean();\n break;\n case 'array':\n zType = z.array(toZodType(schema.items));\n break;\n case 'object':\n zType = z.object(toZodShape(schema));\n break;\n case 'null':\n zType = z.null();\n break;\n default:\n zType = z.any();\n }\n if (schema.description) zType = zType.describe(schema.description);\n if (schema.nullable) zType = zType.nullable();\n return zType;\n}\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"],"mappings":"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","names":[]}
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -608,6 +608,116 @@ function add({
|
|
|
608
608
|
return leftNumber == null && rightNumber == null ? void 0 : (leftNumber ?? 0) + (rightNumber ?? 0);
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
+
// src/bridge/tool-relay-auth.ts
|
|
612
|
+
import { readdir, readFile, readlink } from "fs/promises";
|
|
613
|
+
var ToolRelayAuthorizer = class {
|
|
614
|
+
constructor({
|
|
615
|
+
ttlMs = 1e4,
|
|
616
|
+
now = Date.now
|
|
617
|
+
} = {}) {
|
|
618
|
+
this.authorizations = [];
|
|
619
|
+
this.ttlMs = ttlMs;
|
|
620
|
+
this.now = now;
|
|
621
|
+
}
|
|
622
|
+
authorizeToolCall(call) {
|
|
623
|
+
this.pruneExpired();
|
|
624
|
+
this.authorizations.push({
|
|
625
|
+
key: toolRelayCallKey(call),
|
|
626
|
+
expiresAt: this.now() + this.ttlMs
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
consumeToolCall(call) {
|
|
630
|
+
this.pruneExpired();
|
|
631
|
+
const key = toolRelayCallKey(call);
|
|
632
|
+
const index = this.authorizations.findIndex((auth) => auth.key === key);
|
|
633
|
+
if (index === -1) return false;
|
|
634
|
+
this.authorizations.splice(index, 1);
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
pruneExpired() {
|
|
638
|
+
const now = this.now();
|
|
639
|
+
for (let i = this.authorizations.length - 1; i >= 0; i--) {
|
|
640
|
+
if (this.authorizations[i].expiresAt <= now) {
|
|
641
|
+
this.authorizations.splice(i, 1);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function toolRelayCallKey({ toolName, input }) {
|
|
647
|
+
return `${toolName}\0${canonicalJson(input ?? {})}`;
|
|
648
|
+
}
|
|
649
|
+
function canonicalJson(value) {
|
|
650
|
+
return JSON.stringify(normalizeJsonValue(value));
|
|
651
|
+
}
|
|
652
|
+
function normalizeJsonValue(value) {
|
|
653
|
+
if (Array.isArray(value)) {
|
|
654
|
+
return value.map(normalizeJsonValue);
|
|
655
|
+
}
|
|
656
|
+
if (value && typeof value === "object") {
|
|
657
|
+
return Object.fromEntries(
|
|
658
|
+
Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, normalizeJsonValue(entryValue)])
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return value;
|
|
662
|
+
}
|
|
663
|
+
async function isToolRelayRequestFromAllowedProcess({
|
|
664
|
+
socket,
|
|
665
|
+
allowedScriptPaths
|
|
666
|
+
}) {
|
|
667
|
+
if (process.platform !== "linux") return false;
|
|
668
|
+
if (!socket.remotePort || !socket.localPort) return false;
|
|
669
|
+
const inode = await findTcpSocketInode({
|
|
670
|
+
clientPort: socket.remotePort,
|
|
671
|
+
serverPort: socket.localPort
|
|
672
|
+
});
|
|
673
|
+
if (!inode) return false;
|
|
674
|
+
const cmdline = await findProcessCmdlineForSocketInode({ inode });
|
|
675
|
+
return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
|
|
676
|
+
}
|
|
677
|
+
async function findTcpSocketInode({
|
|
678
|
+
clientPort,
|
|
679
|
+
serverPort
|
|
680
|
+
}) {
|
|
681
|
+
for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
682
|
+
const table = await readFile(tablePath, "utf8").catch(() => void 0);
|
|
683
|
+
if (!table) continue;
|
|
684
|
+
for (const line of table.split("\n").slice(1)) {
|
|
685
|
+
const columns = line.trim().split(/\s+/);
|
|
686
|
+
if (columns.length < 10) continue;
|
|
687
|
+
const local = parseProcNetAddress(columns[1]);
|
|
688
|
+
const remote = parseProcNetAddress(columns[2]);
|
|
689
|
+
if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
|
|
690
|
+
return columns[9];
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
function parseProcNetAddress(value) {
|
|
697
|
+
const [, portHex] = value.split(":");
|
|
698
|
+
if (!portHex) return void 0;
|
|
699
|
+
return { port: Number.parseInt(portHex, 16) };
|
|
700
|
+
}
|
|
701
|
+
async function findProcessCmdlineForSocketInode({
|
|
702
|
+
inode
|
|
703
|
+
}) {
|
|
704
|
+
const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
|
|
705
|
+
() => []
|
|
706
|
+
);
|
|
707
|
+
for (const entry of procEntries) {
|
|
708
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
|
|
709
|
+
const fdDir = `/proc/${entry.name}/fd`;
|
|
710
|
+
const fds = await readdir(fdDir).catch(() => []);
|
|
711
|
+
for (const fd of fds) {
|
|
712
|
+
const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
|
|
713
|
+
if (target !== `socket:[${inode}]`) continue;
|
|
714
|
+
const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
|
|
715
|
+
if (cmdline) return cmdline;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return void 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
611
721
|
// src/bridge/index.ts
|
|
612
722
|
var NATIVE_TO_COMMON = {
|
|
613
723
|
view: "read",
|
|
@@ -681,12 +791,10 @@ async function ensureRuntime({
|
|
|
681
791
|
emit
|
|
682
792
|
}) {
|
|
683
793
|
if (runtime.client) return;
|
|
684
|
-
let relayToken;
|
|
685
794
|
if (start.tools && start.tools.length > 0) {
|
|
686
|
-
relayToken = randomUUID();
|
|
687
795
|
runtime.toolNames = new Set(start.tools.map((tool) => tool.name));
|
|
688
796
|
runtime.relay = await startToolRelay({
|
|
689
|
-
|
|
797
|
+
allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
|
|
690
798
|
tools: start.tools,
|
|
691
799
|
emit,
|
|
692
800
|
requestToolResult: turn.requestToolResult
|
|
@@ -698,7 +806,6 @@ async function ensureRuntime({
|
|
|
698
806
|
timeout: 3e4,
|
|
699
807
|
config: buildOpenCodeConfig({
|
|
700
808
|
start,
|
|
701
|
-
relayToken,
|
|
702
809
|
relayPort: runtime.relay?.port
|
|
703
810
|
})
|
|
704
811
|
});
|
|
@@ -710,7 +817,6 @@ async function ensureRuntime({
|
|
|
710
817
|
}
|
|
711
818
|
function buildOpenCodeConfig({
|
|
712
819
|
start,
|
|
713
|
-
relayToken,
|
|
714
820
|
relayPort
|
|
715
821
|
}) {
|
|
716
822
|
const config = {
|
|
@@ -733,7 +839,7 @@ function buildOpenCodeConfig({
|
|
|
733
839
|
if (skillsDir) config.skills = { paths: [skillsDir] };
|
|
734
840
|
const provider = buildProviderConfig(start);
|
|
735
841
|
if (provider) config.provider = provider;
|
|
736
|
-
if (
|
|
842
|
+
if (relayPort && start.tools && start.tools.length > 0) {
|
|
737
843
|
config.mcp = {
|
|
738
844
|
"harness-tools": {
|
|
739
845
|
type: "local",
|
|
@@ -747,8 +853,7 @@ function buildOpenCodeConfig({
|
|
|
747
853
|
inputSchema: t.inputSchema
|
|
748
854
|
}))
|
|
749
855
|
),
|
|
750
|
-
TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}
|
|
751
|
-
TOOL_RELAY_TOKEN: relayToken
|
|
856
|
+
TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`
|
|
752
857
|
}
|
|
753
858
|
}
|
|
754
859
|
};
|
|
@@ -1165,6 +1270,7 @@ function createTranslationState() {
|
|
|
1165
1270
|
toolNames: /* @__PURE__ */ new Map(),
|
|
1166
1271
|
toolCallsEmitted: /* @__PURE__ */ new Set(),
|
|
1167
1272
|
toolResultsEmitted: /* @__PURE__ */ new Set(),
|
|
1273
|
+
hostToolCallsAuthorized: /* @__PURE__ */ new Set(),
|
|
1168
1274
|
shellCommands: /* @__PURE__ */ new Map(),
|
|
1169
1275
|
messageRoles: /* @__PURE__ */ new Map(),
|
|
1170
1276
|
turnUsage: void 0,
|
|
@@ -1334,7 +1440,16 @@ async function translateAndEmit({
|
|
|
1334
1440
|
const rawToolName = String(props.tool ?? "unknown");
|
|
1335
1441
|
const toolName = toWireToolName(rawToolName);
|
|
1336
1442
|
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1337
|
-
|
|
1443
|
+
const hostToolName = getHostToolName(toolName, props.tool);
|
|
1444
|
+
if (hostToolName) {
|
|
1445
|
+
authorizeHostToolCall({
|
|
1446
|
+
callID,
|
|
1447
|
+
toolName: hostToolName,
|
|
1448
|
+
input: props.input ?? parseToolInput(state, props),
|
|
1449
|
+
state
|
|
1450
|
+
});
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1338
1453
|
emit({
|
|
1339
1454
|
type: "tool-call",
|
|
1340
1455
|
toolCallId: callID,
|
|
@@ -1351,7 +1466,7 @@ async function translateAndEmit({
|
|
|
1351
1466
|
const cachedTool = state.toolNames.get(callID);
|
|
1352
1467
|
const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
|
|
1353
1468
|
const toolName = cachedTool?.toolName ?? toWireToolName(rawToolName || "unknown");
|
|
1354
|
-
if (
|
|
1469
|
+
if (getHostToolName(toolName, rawToolName)) return;
|
|
1355
1470
|
emit({
|
|
1356
1471
|
type: "tool-result",
|
|
1357
1472
|
toolCallId: callID,
|
|
@@ -1514,7 +1629,18 @@ function emitLegacyToolPart({
|
|
|
1514
1629
|
const rawToolName = toolPart.tool;
|
|
1515
1630
|
const toolName = toWireToolName(rawToolName);
|
|
1516
1631
|
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1517
|
-
|
|
1632
|
+
const hostToolName = getHostToolName(toolName, rawToolName);
|
|
1633
|
+
if (hostToolName) {
|
|
1634
|
+
if (status === "running") {
|
|
1635
|
+
authorizeHostToolCall({
|
|
1636
|
+
callID,
|
|
1637
|
+
toolName: hostToolName,
|
|
1638
|
+
input: legacyToolPartInput(toolPart),
|
|
1639
|
+
state
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1518
1644
|
if (!state.toolCallsEmitted.has(callID)) {
|
|
1519
1645
|
state.toolCallsEmitted.add(callID);
|
|
1520
1646
|
emit({
|
|
@@ -1687,15 +1813,25 @@ function nativeNameField({
|
|
|
1687
1813
|
if (!nativeName || nativeName === toolName || toolName === "agent") return {};
|
|
1688
1814
|
return { nativeName };
|
|
1689
1815
|
}
|
|
1690
|
-
function
|
|
1691
|
-
if (runtime.toolNames.has(toolName)) return
|
|
1816
|
+
function getHostToolName(toolName, rawToolName) {
|
|
1817
|
+
if (runtime.toolNames.has(toolName)) return toolName;
|
|
1692
1818
|
if (typeof rawToolName === "string" && runtime.toolNames.has(rawToolName)) {
|
|
1693
|
-
return
|
|
1819
|
+
return rawToolName;
|
|
1694
1820
|
}
|
|
1695
1821
|
if (typeof rawToolName === "string" && rawToolName.startsWith("harness-tools_") && runtime.toolNames.has(rawToolName.slice("harness-tools_".length))) {
|
|
1696
|
-
return
|
|
1822
|
+
return rawToolName.slice("harness-tools_".length);
|
|
1697
1823
|
}
|
|
1698
|
-
return
|
|
1824
|
+
return void 0;
|
|
1825
|
+
}
|
|
1826
|
+
function authorizeHostToolCall({
|
|
1827
|
+
callID,
|
|
1828
|
+
toolName,
|
|
1829
|
+
input,
|
|
1830
|
+
state
|
|
1831
|
+
}) {
|
|
1832
|
+
if (state.hostToolCallsAuthorized.has(callID)) return;
|
|
1833
|
+
state.hostToolCallsAuthorized.add(callID);
|
|
1834
|
+
runtime.relay?.authorizeToolCall({ toolName, input });
|
|
1699
1835
|
}
|
|
1700
1836
|
async function emitContextFallback({
|
|
1701
1837
|
client,
|
|
@@ -1802,15 +1938,17 @@ function mapFinishReason(reason) {
|
|
|
1802
1938
|
return "other";
|
|
1803
1939
|
}
|
|
1804
1940
|
async function startToolRelay({
|
|
1805
|
-
|
|
1941
|
+
allowedScriptPaths,
|
|
1806
1942
|
tools,
|
|
1807
1943
|
emit,
|
|
1808
1944
|
requestToolResult
|
|
1809
1945
|
}) {
|
|
1810
1946
|
const toolNames = new Set(tools.map((t) => t.name));
|
|
1947
|
+
const allowedScriptPathSet = new Set(allowedScriptPaths);
|
|
1948
|
+
const authorizer = new ToolRelayAuthorizer();
|
|
1811
1949
|
const server = createServer(async (req, res) => {
|
|
1812
1950
|
try {
|
|
1813
|
-
if (req.method !== "POST" || req.url !== "/"
|
|
1951
|
+
if (req.method !== "POST" || req.url !== "/") {
|
|
1814
1952
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1815
1953
|
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1816
1954
|
return;
|
|
@@ -1828,6 +1966,15 @@ async function startToolRelay({
|
|
|
1828
1966
|
);
|
|
1829
1967
|
return;
|
|
1830
1968
|
}
|
|
1969
|
+
const authorized = authorizer.consumeToolCall({ toolName, input }) || await isToolRelayRequestFromAllowedProcess({
|
|
1970
|
+
socket: req.socket,
|
|
1971
|
+
allowedScriptPaths: allowedScriptPathSet
|
|
1972
|
+
});
|
|
1973
|
+
if (!authorized) {
|
|
1974
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1975
|
+
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1831
1978
|
emit({
|
|
1832
1979
|
type: "tool-call",
|
|
1833
1980
|
toolCallId: requestId,
|
|
@@ -1863,7 +2010,8 @@ async function startToolRelay({
|
|
|
1863
2010
|
}
|
|
1864
2011
|
return {
|
|
1865
2012
|
port: address.port,
|
|
1866
|
-
close: () => closeServer(server)
|
|
2013
|
+
close: () => closeServer(server),
|
|
2014
|
+
authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
|
|
1867
2015
|
};
|
|
1868
2016
|
}
|
|
1869
2017
|
function closeServer(server) {
|