memory-mcp-configs.spec.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * The third-party memory examples stay config-only. This suite parses every
  3. * checked-in overlay, verifies its package pin, transport, and secret handling, then replaces
  4. * only the upstream endpoint with the package-owned keyless MCP fixture and
  5. * proves the real Cordis Loader discovers a tool through the generic bridge.
  6. */
  7. import { readFileSync } from 'node:fs'
  8. import { resolve } from 'node:path'
  9. import { afterEach, describe, expect, it } from 'vitest'
  10. import type { Context } from '@deepseek-ai/cordis'
  11. import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
  12. import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
  13. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  14. import ToolRuntime from '@deepseek-ai/dsh-tools'
  15. import * as McpClient from '@deepseek-ai/dsh-mcp-client/src/index.ts'
  16. interface ExampleContract {
  17. file: string
  18. id: string
  19. serverName: string
  20. transport: 'stdio' | 'streamable-http'
  21. pin: string
  22. }
  23. interface InsertedRow {
  24. id?: string
  25. name?: string
  26. config?: Record<string, unknown>
  27. }
  28. const root = resolve(import.meta.dirname, '../../..')
  29. const exampleDir = resolve(root, 'apps/cli/config/examples/mcp-memory')
  30. const baseConfig = resolve(import.meta.dirname, 'fixtures/memory-mcp-base.cordis.yml')
  31. const fixtureServer = resolve(root, 'packages/mcp/mcp-client/tests/fixture-server.ts')
  32. const examples: ExampleContract[] = [
  33. {
  34. file: 'memorix.cordis.yml',
  35. id: 'memory-memorix',
  36. serverName: 'memorix',
  37. transport: 'stdio',
  38. pin: '1.3.0',
  39. },
  40. {
  41. file: 'mcp-reference-memory.cordis.yml',
  42. id: 'memory-mcp-reference',
  43. serverName: 'reference_memory',
  44. transport: 'stdio',
  45. pin: '2026.7.4',
  46. },
  47. {
  48. file: 'engram.cordis.yml',
  49. id: 'memory-engram',
  50. serverName: 'engram',
  51. transport: 'stdio',
  52. pin: '1.20.0',
  53. },
  54. ]
  55. const liveContexts = new Set<Context>()
  56. afterEach(async () => {
  57. await Promise.all([...liveContexts].map(async ctx => ctx.fiber.dispose()))
  58. liveContexts.clear()
  59. })
  60. function insertedRow(patches: PatchOptions[]): InsertedRow {
  61. expect(patches).toHaveLength(1)
  62. const insert = patches[0]?.insert
  63. expect(insert).toHaveLength(1)
  64. return insert?.[0] as InsertedRow
  65. }
  66. async function waitForTool(ctx: Context, name: string): Promise<void> {
  67. const deadline = Date.now() + 10_000
  68. while (!ctx.tools.schemas().some(schema => schema.name === name)) {
  69. if (Date.now() >= deadline) throw new Error(`timed out waiting for ${name}`)
  70. await new Promise(resolveWait => setTimeout(resolveWait, 25))
  71. }
  72. }
  73. describe('third-party memory MCP example overlays', () => {
  74. it.each(examples)('parses $file with the documented generic plugin fields', (contract) => {
  75. const file = resolve(exampleDir, contract.file)
  76. const source = readFileSync(file, 'utf8')
  77. const row = insertedRow(loadOverlayPatches('memory-mcp-config-test', file))
  78. expect(row.id).toBe(contract.id)
  79. expect(row.name).toBe('@deepseek-ai/dsh-mcp-client')
  80. expect(row.config?.serverName).toBe(contract.serverName)
  81. expect(row.config?.transport).toBe(contract.transport)
  82. expect(source.split('\n', 1)[0]).toContain(contract.pin)
  83. expect(source).not.toMatch(/\bsk-[A-Za-z0-9_-]{8,}\b/)
  84. expect(source).not.toContain('DEEPSEEK_API_KEY')
  85. })
  86. it.each(examples)('loads $file and discovers a keyless fixture tool', async (contract) => {
  87. const patches = loadOverlayPatches(
  88. 'memory-mcp-config-test',
  89. resolve(exampleDir, contract.file),
  90. )
  91. // The static config gate verifies the checked-in bare package specifier.
  92. // The unit test maps it to the source module so a clean checkout needs no
  93. // prebuilt `lib/` artifacts before proving the Loader/MCP behavior.
  94. insertedRow(patches).name = 'cordis:memory-test-mcp-client'
  95. const fixturePatch: PatchOptions = {
  96. id: contract.id,
  97. config: {
  98. serverName: contract.serverName,
  99. transport: 'stdio',
  100. command: process.execPath,
  101. args: [fixtureServer],
  102. env: {},
  103. cwd: root,
  104. toolCallTimeoutMs: 5_000,
  105. },
  106. }
  107. const ctx = await boot(
  108. 'memory-mcp-config-test',
  109. baseConfig,
  110. [...patches, fixturePatch],
  111. (ctx) => {
  112. liveContexts.add(ctx)
  113. ctx.loader.builtins['memory-test-system-prompt'] = SystemPrompt
  114. ctx.loader.builtins['memory-test-tools'] = ToolRuntime
  115. ctx.loader.builtins['memory-test-mcp-client'] = McpClient
  116. },
  117. )
  118. await waitForTool(ctx, `mcp__${contract.serverName}__greet`)
  119. }, 15_000)
  120. })