74 lines
4.5 KiB
JavaScript
74 lines
4.5 KiB
JavaScript
/** External SDK consumer. No application/store/runtime/database imports. */
|
|
import assert from 'node:assert/strict'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
|
|
|
|
const endpoint = new URL(process.env.OWN_MCP_ENDPOINT)
|
|
assert.equal(process.env.OWN_EXTERNAL_CLIENT, 'true')
|
|
assert.equal(endpoint.hostname, '127.0.0.1')
|
|
assert.notEqual(process.pid, Number(process.env.OWN_SERVER_PID))
|
|
const client = new Client({ name: 'owned-linked-bridge-vlan-sdk', version: '1.0.0' })
|
|
const boundedFetch = async (input, init) => {
|
|
const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url)
|
|
assert.equal(url.origin, endpoint.origin)
|
|
return fetch(input, { ...init, redirect: 'manual', signal: AbortSignal.timeout(10000) })
|
|
}
|
|
const payload = result => {
|
|
const text = result.content.find(item => item.type === 'text')?.text
|
|
assert(text && Buffer.byteLength(text) < 131072)
|
|
return JSON.parse(text)
|
|
}
|
|
const call = async (name, args) => {
|
|
const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 10000 })
|
|
assert(!result.isError, JSON.stringify(payload(result)))
|
|
return payload(result)
|
|
}
|
|
try {
|
|
await client.connect(new StreamableHTTPClientTransport(endpoint, {
|
|
requestInit: { headers: { authorization: 'Bearer ' + process.env.OWN_MCP_TOKEN } }, fetch: boundedFetch,
|
|
}), { timeout: 10000 })
|
|
const tools = (await client.listTools({}, { timeout: 10000 })).tools
|
|
const read = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_get')
|
|
const configure = tools.find(tool => tool.name === 'mm_bridge_vlan_configuration_plan')
|
|
assert(read && configure)
|
|
assert.match(read.description, /Полная конфигурация bridge VLAN/)
|
|
assert.match(read.description, /Возвращает данные только из разрешённой области/)
|
|
assert.match(configure.description, /Связанный план VLAN membership, PVID и bridge filtering/)
|
|
assert.match(configure.description, /Подготавливает неизменяемый план/)
|
|
assert.equal(read.annotations.readOnlyHint, true)
|
|
assert.equal(configure.annotations.readOnlyHint, false)
|
|
assert.deepEqual(read.inputSchema.required, ['serverId'])
|
|
assert.deepEqual(configure.inputSchema.required, ['serverId', 'bridgeId', 'desired'])
|
|
const desiredSchema = configure.inputSchema.properties.desired
|
|
for (const name of ['ports', 'vlans', 'removePortIds', 'removeVlanIds']) assert(desiredSchema.properties[name])
|
|
const serverId = Number(process.env.OWN_ALLOWED_SERVER)
|
|
const state = (await call(read.name, { serverId })).state
|
|
assert.equal(state.bridges[0]['.id'], '*1')
|
|
assert(state.interfaces.some(item => item.resource === 'ethernet' && item.id === '*3' && item.name === 'ether3'))
|
|
assert.equal(state.vlans[0]['vlan-ids'], '10')
|
|
const cpu = { resource: 'bridge', id: '*1' }, access = { resource: 'ethernet', id: '*2' }, trunk = { resource: 'ethernet', id: '*3' }
|
|
const plan = await call(configure.name, { serverId, bridgeId: '*1', desired: {
|
|
filtering: true, pvid: 1,
|
|
ports: [{ id: '*10', interface: access, pvid: 10, ingressFiltering: true, frameTypes: 'admit-all', disabled: false },
|
|
{ interface: trunk, pvid: 1, ingressFiltering: true, frameTypes: 'admit-only-vlan-tagged', disabled: false }],
|
|
vlans: [{ id: '*11', vlanIds: '10', tagged: [cpu], untagged: [access], disabled: false },
|
|
{ vlanIds: '20', tagged: [cpu, trunk], untagged: [], disabled: false }], removePortIds: [], removeVlanIds: [],
|
|
} })
|
|
assert.equal(plan.state, 'awaiting-approval')
|
|
assert.equal(typeof plan.planId, 'string')
|
|
assert.equal(plan.changes.requiresHuman, true)
|
|
assert.deepEqual(plan.changes.steps.map(item => [item.step.resource, item.step.action]),
|
|
[['bridge_vlan', 'create'], ['bridge_port', 'create'], ['bridge', 'update']])
|
|
const denied = await client.callTool({ name: 'mm_change_apply', arguments: { planId: plan.planId, idempotencyKey: randomUUID() } }, undefined, { timeout: 10000 })
|
|
assert.equal(denied.isError, true)
|
|
const deniedPayload = payload(denied)
|
|
assert(JSON.stringify(deniedPayload).includes('APPROVAL_REQUIRED'))
|
|
const readback = (await call(read.name, { serverId })).state
|
|
assert.deepEqual(readback, state)
|
|
process.send({ kind: 'result', result: { externalSdkProcess: true, transport: 'streamable-http', clientPid: process.pid,
|
|
listedTools: [read, configure], state, plan, applyDenial: deniedPayload, unchangedReadback: true } })
|
|
} finally {
|
|
await client.close()
|
|
process.disconnect?.()
|
|
}
|