DeepWiki 커넥터로 데이터베이스 어드바이저 에이전트 만들기

DeepWiki 커넥터로 데이터베이스 어드바이저 에이전트 만들기 (TypeScript) (Build a Database Advisor Agent with the DeepWiki Connector, TypeScript)

쓰기 부하가 많은 로컬 분석에 쓸 데이터베이스가 필요하다고 가정해 볼게요. SQLite, DuckDB, LevelDB 모두 강력한 후보지만, 실제로 어느 것이 맞을까요? 문서를 손으로 읽는 대신, 이 스크립트는 Mistral Agent가 DeepWiki 커넥터를 통해 실제 소스 코드를 읽고 결정하게 해줘요. 이 스크립트는 TypeScript로 같은 에이전트를 만드는 전체 커넥터 수명주기를 보여줘요.

출처: 문서

본문

쓰기 부하가 많은 로컬 분석을 위한 데이터베이스가 필요하다고 가정해 볼게요. SQLite, DuckDB, LevelDB 모두 강력한 후보지만, 실제로 어느 것이 맞을까요? 문서를 손으로 읽기보다는, 이 스크립트는 Mistral Agent가 DeepWiki 커넥터를 통해 그들의 실제 소스 코드를 읽고 결정하게 해요.

이 스크립트는 전체 Mistral Connector 수명주기를 보여줘요.

Step Operation What happens
1 Create Register a connector for each database candidate
2 List Verify all three are registered
3 Use Build an agent that compares them via their GitHub repos
4 Update Mark the winner's connector as selected
5 Delete Clean up the losing connectors

API 상태 (API status): 이 스크립트는 client.beta.connectors와 client.beta.agents를 사용해요. 이들은 베타(beta) 엔드포인트로 변경될 수 있어요.

같은 에이전트의 Python 버전도 이쪽에서 볼 수 있어요.

사전 준비사항 (Prerequisites)

이 쿡북을 완료하려면 다음이 필요해요.

  • Node.js와 패키지 매니저(npm, pnpm 또는 yarn)
  • Mistral 계정과 API 키

환경 설정 (Environment setup)

설치 (Install)

.env 파일에서 API 키를 로드하기 위해 Mistral TypeScript SDK와 dotenv를 설치하는 방법은 다음 중 하나를 사용해요.

npm:

npm install @mistralai/mistralai dotenv

pnpm:

pnpm add @mistralai/mistralai dotenv

yarn:

yarn add @mistralai/mistralai dotenv

필요한 환경 변수 (Required environment variables)

이 쿡북을 완료하려면 Mistral API 키가 필요해요. Studio에서 API keys 섹션으로 이동해서, Connector access scope에 대해 Private and shared connectors를 선택하고 새 API 키를 만들어요.

프로젝트 루트에 .env를 만들고 Mistral API 키를 추가해요.

MISTRAL_API_KEY=your-mistral-api-key

1단계 — 설정 (Step 1 — Setup)

프로젝트 디렉터리에 build-a-database-advisor-agent.ts를 만들어요.

touch build-a-database-advisor-agent.ts

파일을 열고 클라이언트, DeepWiki 서버 URL, 후보 목록, 응답 스키마를 추가해요. 남은 단계들은 main 함수의 try와 finally 블록을 채워 나가요.

응답 스키마는 에이전트가 반환해야 하는 정확한 JSON 구조를 정의해요. 타이핑된 상수로 정의하면 TypeScript 타입 소스이자 API에 전달되는 스키마 역할을 해서, 중복이 없어요.

import "dotenv/config";
import { Mistral } from "@mistralai/mistralai";

const client = new Mistral({ apiKey: proces..._KEY });

const DEEPWIKI_URL = "https://mcp.deepwiki.com/mcp";

const candidates = [
  { name: "showdown_sqlite",  description: "DeepWiki connector — sqlite/sqlite" },
  { name: "showdown_duckdb",  description: "DeepWiki connector — duckdb/duckdb" },
  { name: "showdown_leveldb", description: "DeepWiki connector — google/leveldb" },
];

// The JSON schema the agent must conform to. Passed to the API via completionArgs
// so the structure is enforced at the API level, not just by prompt instructions.
const RESPONSE_SCHEMA = {
  type: "object",
  properties: {
    queries: {
      type: "array",
      items: {
        type: "object",
        properties: {
          connector: { type: "string" },
          question:  { type: "string" },
          summary:   { type: "string" },
        },
        required: ["connector", "question", "summary"],
      },
    },
    comparison: {
      type: "object",
      properties: {
        storage_model:      { type: "string" },
        acid_guarantees:    { type: "string" },
        query_capabilities: { type: "string" },
        write_throughput:   { type: "string" },
        python_api:         { type: "string" },
      },
      required: ["storage_model", "acid_guarantees", "query_capabilities", "write_throughput", "python_api"],
    },
    reasoning:      { type: "string" },
    recommendation: { type: "string", enum: ["showdown_sqlite", "showdown_duckdb", "showdown_leveldb"] },
  },
  required: ["queries", "comparison", "reasoning", "recommendation"],
} as const;

interface ComparisonResult {
  queries: { connector: string; question: string; summary: string }[];
  comparison: Record<string, string>;
  reasoning: string;
  recommendation: string;
}

async function main(): Promise<void> {
  let agentId: string | undefined;
  const connectorIds: Record<string, string> = {};

  try {
    // Step 2 — Create one connector per candidate
    // Step 3 — List to verify
    // Step 4 — Build the comparison agent
    // Step 5 — Run the comparison
    // Step 6 — Promote the winner, retire the rest
  } finally {
    // Cleanup — delete the agent
  }
}

main().catch(console.error);

2단계 — 후보마다 커넥터 하나 생성 (Step 2 — Create one connector per candidate)

각 커넥터는 DeepWiki MCP 서버를 가리켜서, Mistral이 공개 GitHub 저장소를 읽고 추론할 수 있게 해요. 데이터베이스당 하나씩 세 개의 이름 있는 커넥터가 에이전트에게 독립적으로 쿼리할 슬롯을 제공해요.

각 커넥터를 만든 후 createOrUpdateUserCredentials로 자격 증명을 등록해요. DeepWiki는 인증이 필요 없는 공개 서버라 credentials는 빈 객체지만, 커넥터를 쿼리하려면 자격 증명 레코드가 여전히 존재해야 해요.

// Step 2 — Create one connector per candidate를 다음으로 교체해요.

    // Step 2 — Create one connector per candidate
    for (const c of candidates) {
      const connector = await client.beta.connectors.create({
        name: c.name,
        description: c.description,
        server: DEEPWIKI_URL,
        visibility: "private",
      });
      connectorIds[c.name] = connector.id;
      console.log(`Created: ${connector.name}  (id=${connector.id})`);
      await client.beta.connectors.createOrUpdateUserCredentials({
        connectorIdOrName: connector.name,
        credentialsCreateOrUpdate: {
          name: `${connector.name}-default`,
          credentials: { headers: {} },
          isDefault: true,
        },
      });
      console.log(`  Credentials registered for ${connector.name}`);
    }

등록된 Connector를 Studio에서 확인할 수 있어요.

3단계 — 목록으로 확인 (Step 3 — List to verify)

세 커넥터가 모두 등록됐는지 확인한 다음, 각각에 listTools를 호출해서 자격 증명이 작동하는지 확인하고 커넥터가 노출하는 도구를 살펴보아요.

// Step 3 — List to verify를 다음으로 교체해요.

    // Step 3 — List to verify
    const page = await client.beta.connectors.list({ pageSize: 50 });
    const showdown = (page.items ?? []).filter((c) =>
      c.name?.startsWith("showdown_")
    );
    console.log(`${showdown.length} showdown connectors registered:`);
    for (const c of showdown) {
      console.log(`  ${(c.name ?? "").padEnd(22)}  ${c.description}`);
      const tools = await client.beta.connectors.listTools({
        connectorIdOrName: c.name ?? "",
      });
      for (const tool of tools) {
        console.log(`    - ${tool.name}: ${tool.description}`);
      }
    }

4단계 — 비교 에이전트 만들기 (Step 4 — Build the comparison agent)

세 개의 커넥터를 모두 연결한 Mistral 에이전트를 만들어요. 지시사항은 원시 소스 파일을 읽지 않고 각 커넥터에 자연어 질문을 하라고 알려줘요 — 이렇게 하면 응답이 컨텍스트 창에 들어갈 만큼 간결해져요. completionArgs.responseFormat을 통해 RESPONSE_SCHEMA를 전달하면 JSON 구조가 API 수준에서 강제되므로, 추천 필드는 항상 유효한 커넥터 이름이고 출력은 항상 regex 없이 파싱 가능해요.

// Step 4 — Build the comparison agent를 다음으로 교체해요.

    // Step 4 — Build the comparison agent
    const agent = await client.beta.agents.create({
      name: "Database Showdown Judge",
      description: "Compares database candidates using their source code via DeepWiki.",
      model: "mistral-medium-latest",
      instructions:
        "You are a database selection expert. " +
        "When given a comparison task, call each DeepWiki connector once with a focused " +
        "natural-language question about the repository — do NOT read raw source files.",
      completionArgs: {
        responseFormat: {
          type: "json_schema" as const,
          jsonSchema: {
            name: "comparison_result",
            schemaDefinition: RESPONSE_SCHEMA,
            strict: true,
          },
        },
      },
      tools: candidates.map((c) => ({
        type: "connector" as const,
        connectorId: connectorIds[c.name],
      })),
    });
    agentId = agent.id;
    console.log(`Agent ready: ${agent.name}  (id=${agent.id})`);

Studio에서 에이전트를 확인할 수 있어요.

5단계 — 비교 실행 (Step 5 — Run the comparison)

이 단계는 의도적으로 두 개의 호출을 사용해요. startStream은 연결을 열어서 커넥터 호출이 실시간으로 일어나는 것을 볼 수 있게 해줘요 — tool.execution.started는 어떤 데이터베이스가 쿼리 중인지 보여주고, tool.execution.done은 결과가 돌아왔음을 확인해줘요. 하지만 스트리밍 API는 모든 에이전트 턴 출력(도구 호출 결정, 중간 요약, 최종 답변)을 "is final" 플래그 없이 동일한 message.output.delta 이벤트로 보내기 때문에, 스트림에서 직접 json_schema로 제한된 응답을 안정적으로 추출할 수 없어요.

getMessages가 이 문제를 깔끔하게 해결해요. 완료된 대화에 대한 구조화된 MessageOutputEntry 객체를 반환하므로, 마지막 어시스턴트 메시지가 명확하게 최종 JSON 답변입니다 — 파싱 우회책이 필요 없어요.

// Step 5 — Run the comparison을 다음으로 교체해요.

    // Step 5 — Run the comparison
    // startStream keeps the connection alive and lets you observe connector
    // calls in real time. We capture the conversation_id from the first event
    // so we can retrieve the final output after the stream ends.
    const stream = await client.beta.conversations.startStream(
      {
        agentId: agent.id,
        inputs: [
          {
            role: "user",
            content:
              "Compare sqlite/sqlite, duckdb/duckdb, and google/leveldb for a write-heavy " +
              "local analytics workload. Evaluate storage model, ACID guarantees, query " +
              "capabilities, write throughput, and Python API simplicity. Recommend one.",
          },
        ],
      },
      { timeoutMs: 300_000 },
    );

    let conversationId: string | undefined;
    for await (const item of stream) {
      const data = item.data;
      const eventType = data.type;
      const name = (data as any).name ?? "";
      if (eventType === "conversation.response.started") {
        conversationId = (data as any).conversationId;
      } else if (eventType !== "message.output.delta") {
        console.log(`[${eventType}]${name ? ` ${name}` : ""}`);
      }
    }

    // getMessages returns the completed conversation entries. The last
    // message.output entry is the agent's final answer, which is guaranteed
    // to match RESPONSE_SCHEMA because json_schema was set on the agent.
    const messages = await client.beta.conversations.getMessages({
      conversationId: conversationId!,
    });
    const lastOutput = [...(messages.messages ?? [])].reverse()
      .find((m) => (m as any).type === "message.output") as any;
    const rawContent = lastOutput.content;
    const rawText = typeof rawContent === "string"
      ? rawContent
      : (rawContent as any[]).map((c) => c.text ?? "").join("");
    const result = lastJsonIn(rawText) as ComparisonResult;

    console.log("\n--- Connector queries ---");
    for (const q of result.queries) {
      console.log(`\n  [${q.connector}]`);
      console.log(`  Q: ${q.question}`);
      console.log(`  A: ${q.summary}`);
    }

    console.log("\n--- Comparison ---");
    for (const [key, val] of Object.entries(result.comparison)) {
      console.log(`  ${key}: ${val}`);
    }

    console.log(`\n--- Reasoning ---\n  ${result.reasoning}`);
    console.log(`\n--- Recommendation ---\n  ${result.recommendation}`);

    // Extract winner and losers directly from the parsed JSON
    const winnerName = result.recommendation;
    const loserNames = candidates.map((c) => c.name).filter((n) => n !== winnerName);

    console.log(`\nWinner: ${winnerName}`);
    console.log(`Losers: ${loserNames.join(", ")}`);

6단계 — 승자 승격, 나머지 은퇴 (Step 6 — Promote the winner, retire the rest)

승리한 커넥터의 설명을 업데이트해서 선택됨을 표시한 다음, 진 커넥터들을 삭제해요. 이로써 전체 수명주기(create → list → use → update → delete)가 완료돼요.

// Step 6 — Promote the winner, retire the rest를 다음으로 교체해요.

    // Step 6 — Promote the winner, retire the rest
    const winnerDescription =
      candidates.find((c) => c.name === winnerName)?.description ?? "";

    const updated = await client.beta.connectors.update({
      connectorId: connectorIds[winnerName],
      updateConnectorRequest: {
        description: `[SELECTED] ${winnerDescription}`,
      },
    });
    console.log(`Updated:  ${updated.name}  —  ${updated.description}`);

    for (const name of loserNames) {
      const deleteResult = await client.beta.connectors.delete({
        connectorId: connectorIds[name],
      });
      console.log(`Deleted:  ${name}  —  ${deleteResult.message}`);
    }

    // Confirm the winner is still registered with its updated description
    const winner = await client.beta.connectors.get({
      connectorIdOrName: winnerName,
    });
    console.log(`\nWinner confirmed:`);
    console.log(`  Name:        ${winner.name}`);
    console.log(`  Description: ${winner.description}`);
    console.log(`  ID:          ${winner.id}`);

정리 (Cleanup)

작업이 끝나면 에이전트를 삭제해요. finally 블록의 // Cleanup — delete the agent를 다음으로 교체해요.

    // Cleanup — delete the agent
    if (agentId) {
      await client.beta.agents.delete({ agentId });
      console.log(`\nAgent deleted: ${agentId}`);
    }
    // To also remove the winning connector, capture winnerName before the
    // finally block and uncomment:
    // await client.beta.connectors.delete({ connectorId: connectorIds[winnerName] });

실행 (Run)

모든 단계가 준비되면 스크립트를 실행해요.

npx tsx build-a-database-advisor-agent.ts

tsx를 dev dependency로 설치했다면 npm start로도 실행할 수 있어요.

완전한 스크립트 (Complete script)

참고용으로 모든 단계가 결합된 전체 스크립트입니다. GitHub에서도 완전한 프로젝트를 볼 수 있어요.

import "dotenv/config";
import { Mistral } from "@mistralai/mistralai";

const client = new Mistral({ apiKey: proces..._KEY });

const DEEPWIKI_URL = "https://mcp.deepwiki.com/mcp";

const candidates = [
  { name: "showdown_sqlite",  description: "DeepWiki connector — sqlite/sqlite" },
  { name: "showdown_duckdb",  description: "DeepWiki connector — duckdb/duckdb" },
  { name: "showdown_leveldb", description: "DeepWiki connector — google/leveldb" },
];

const RESPONSE_SCHEMA = {
  type: "object",
  properties: {
    queries: {
      type: "array",
      items: {
        type: "object",
        properties: {
          connector: { type: "string" },
          question:  { type: "string" },
          summary:   { type: "string" },
        },
        required: ["connector", "question", "summary"],
      },
    },
    comparison: {
      type: "object",
      properties: {
        storage_model:      { type: "string" },
        acid_guarantees:    { type: "string" },
        query_capabilities: { type: "string" },
        write_throughput:   { type: "string" },
        python_api:         { type: "string" },
      },
      required: ["storage_model", "acid_guarantees", "query_capabilities", "write_throughput", "python_api"],
    },
    reasoning:      { type: "string" },
    recommendation: { type: "string", enum: ["showdown_sqlite", "showdown_duckdb", "showdown_leveldb"] },
  },
  required: ["queries", "comparison", "reasoning", "recommendation"],
} as const;

// The agent can produce multiple message outputs across turns (e.g. an
// intermediate summary before the final answer). The API stores them
// concatenated in the content field regardless of retrieval method. This
// helper scans for all JSON objects and returns the last one, which is always
// the json_schema-constrained final answer.
function lastJsonIn(s: string): unknown {
  let remaining = s;
  let last: unknown;
  while (remaining) {
    const i = remaining.indexOf("{");
    if (i === -1) break;
    remaining = remaining.slice(i);
    try {
      last = JSON.parse(remaining);
      break;
    } catch (e: any) {
      const pos = e.message?.match(/position (\d+)/)?.[1];
      if (pos !== undefined) {
        last = JSON.parse(remaining.slice(0, Number(pos)));
        remaining = remaining.slice(Number(pos));
      } else {
        remaining = remaining.slice(1);
      }
    }
  }
  if (last === undefined) throw new Error("No JSON object found in response");
  return last;
}

interface ComparisonResult {
  queries: { connector: string; question: string; summary: string }[];
  comparison: Record<string, string>;
  reasoning: string;
  recommendation: string;
}

async function main(): Promise<void> {
  let agentId: string | undefined;
  const connectorIds: Record<string, string> = {};

  try {
    // Step 2 — Create one connector per candidate
    for (const c of candidates) {
      const connector = await client.beta.connectors.create({
        name: c.name,
        description: c.description,
        server: DEEPWIKI_URL,
        visibility: "private",
      });
      connectorIds[c.name] = connector.id;
      console.log(`Created: ${connector.name}  (id=${connector.id})`);
      await client.beta.connectors.createOrUpdateUserCredentials({
        connectorIdOrName: connector.name,
        credentialsCreateOrUpdate: {
          name: `${connector.name}-default`,
          credentials: { headers: {} },
          isDefault: true,
        },
      });
      console.log(`  Credentials registered for ${connector.name}`);
    }

    // Step 3 — List to verify
    const page = await client.beta.connectors.list({ pageSize: 50 });
    const showdown = (page.items ?? []).filter((c) =>
      c.name?.startsWith("showdown_")
    );
    console.log(`${showdown.length} showdown connectors registered:`);
    for (const c of showdown) {
      console.log(`  ${(c.name ?? "").padEnd(22)}  ${c.description}`);
      const tools = await client.beta.connectors.listTools({
        connectorIdOrName: c.name ?? "",
      });
      for (const tool of tools) {
        console.log(`    - ${tool.name}: ${tool.description}`);
      }
    }

    // Step 4 — Build the comparison agent
    const agent = await client.beta.agents.create({
      name: "Database Showdown Judge",
      description: "Compares database candidates using their source code via DeepWiki.",
      model: "mistral-medium-latest",
      instructions:
        "You are a database selection expert. " +
        "When given a comparison task, call each DeepWiki connector once with a focused " +
        "natural-language question about the repository — do NOT read raw source files.",
      completionArgs: {
        responseFormat: {
          type: "json_schema" as const,
          jsonSchema: {
            name: "comparison_result",
            schemaDefinition: RESPONSE_SCHEMA,
            strict: true,
          },
        },
      },
      tools: candidates.map((c) => ({
        type: "connector" as const,
        connectorId: connectorIds[c.name],
      })),
    });
    agentId = agent.id;
    console.log(`Agent ready: ${agent.name}  (id=${agent.id})`);

    // Step 5 — Run the comparison
    const stream = await client.beta.conversations.startStream(
      {
        agentId: agent.id,
        inputs: [
          {
            role: "user",
            content:
              "Compare sqlite/sqlite, duckdb/duckdb, and google/leveldb for a write-heavy " +
              "local analytics workload. Evaluate storage model, ACID guarantees, query " +
              "capabilities, write throughput, and Python API simplicity. Recommend one.",
          },
        ],
      },
      { timeoutMs: 300_000 },
    );

    let conversationId: string | undefined;
    for await (const item of stream) {
      const data = item.data;
      const eventType = data.type;
      const name = (data as any).name ?? "";
      if (eventType === "conversation.response.started") {
        conversationId = (data as any).conversationId;
      } else if (eventType !== "message.output.delta") {
        console.log(`[${eventType}]${name ? ` ${name}` : ""}`);
      }
    }

    const messages = await client.beta.conversations.getMessages({
      conversationId: conversationId!,
    });
    const lastOutput = [...(messages.messages ?? [])].reverse()
      .find((m) => (m as any).type === "message.output") as any;
    const rawContent = lastOutput.content;
    const rawText = typeof rawContent === "string"
      ? rawContent
      : (rawContent as any[]).map((c) => c.text ?? "").join("");
    const result = lastJsonIn(rawText) as ComparisonResult;

    console.log("\n--- Connector queries ---");
    for (const q of result.queries) {
      console.log(`\n  [${q.connector}]`);
      console.log(`  Q: ${q.question}`);
      console.log(`  A: ${q.summary}`);
    }

    console.log("\n--- Comparison ---");
    for (const [key, val] of Object.entries(result.comparison)) {
      console.log(`  ${key}: ${val}`);
    }

    console.log(`\n--- Reasoning ---\n  ${result.reasoning}`);
    console.log(`\n--- Recommendation ---\n  ${result.recommendation}`);

    const winnerName = result.recommendation;
    const loserNames = candidates.map((c) => c.name).filter((n) => n !== winnerName);

    console.log(`\nWinner: ${winnerName}`);
    console.log(`Losers: ${loserNames.join(", ")}`);

    // Step 6 — Promote the winner, retire the rest
    const winnerDescription =
      candidates.find((c) => c.name === winnerName)?.description ?? "";

    const updated = await client.beta.connectors.update({
      connectorId: connectorIds[winnerName],
      updateConnectorRequest: {
        description: `[SELECTED] ${winnerDescription}`,
      },
    });
    console.log(`Updated:  ${updated.name}  —  ${updated.description}`);

    for (const name of loserNames) {
      const deleteResult = await client.beta.connectors.delete({
        connectorId: connectorIds[name],
      });
      console.log(`Deleted:  ${name}  —  ${deleteResult.message}`);
    }

    const winner = await client.beta.connectors.get({
      connectorIdOrName: winnerName,
    });
    console.log(`\nWinner confirmed:`);
    console.log(`  Name:        ${winner.name}`);
    console.log(`  Description: ${winner.description}`);
    console.log(`  ID:          ${winner.id}`);
  } finally {
    // Cleanup — delete the agent
    if (agentId) {
      await client.beta.agents.delete({ agentId });
      console.log(`\nAgent deleted: ${agentId}`);
    }
    // To also remove the winning connector, uncomment:
    // await client.beta.connectors.delete({ connectorId: connectorIds[winnerName] });
  }
}

main().catch(console.error);

요약 (Summary)

이 스크립트는 DeepWiki Connector를 사용해 모델이 GitHub 저장소 소스 코드에 대해 자연어 질문을 하고 데이터 기반의 데이터베이스 추천을 내도록 하면서, Mistral Connector의 전체 수명주기(create, list, use, update, delete)를 보여줬어요.

만든 것 (What you built):

  • DeepWiki MCP 서버를 가리키는 세 개의 이름 있는 Connector
  • completionArgs를 통해 json_schema 응답 형식이 강제된, 세 개의 Connector가 연결된 에이전트(Database Showdown Judge)
  • 커넥터 도구 호출을 기록하고, 구조화된 JSON 추천을 파싱하고, 승자의 Connector를 업데이트하고, 나머지를 정리한 대화

사용한 Mistral 기능 (Mistral features used):

  • Connectors (beta)
  • Agents API (beta)
  • Conversations API (beta)

기타 서비스 (Other services):

  • DeepWiki — 공개 GitHub 저장소를 읽기 위한 MCP 서버

Connector를 Studio에서 확인할 수 있어요.

더 알아보기 (Learn more)