Connectors와 웹 검색으로 퀵스타트 생성기 만들기
Connectors와 웹 검색으로 퀵스타트 생성기 만들기 (TypeScript)
Mistral 커넥터(Connectors)와 웹 검색(Web Search) 도구를 활용해 Polars 라이브러리 퀵스타트를 자동 생성하는 TypeScript 스크립트를 만드는 쿡북이에요. 같은 프롬프트를 5가지 도구 구성으로 각각 실행해, 모델에 좋은 소스를 줄수록 출력 품질이 어떻게 달라지는지 비교합니다.
출처: 문서
본문
Polars(파이썬용 고속 DataFrame 라이브러리)를 배우고 싶다고 가정해요. 문서를 읽고, 블로그 글을 훑고, 직접 퀵스타트를 조립하는 대신 — 올바른 도구에 접근할 수 있는 Mistral에게 시킬 수 있어요.
이 스크립트는 같은 프롬프트를 5번 보내는데, 매번 도구 구성을 다르게 해서 모델에게 더 좋은 소스를 줄수록 출력 품질이 어떻게 좋아지는지 확인할 수 있어요:
| 단계 | 도구 | 모델이 접근할 수 있는 것 |
|---|---|---|
| 1 | 없음 | 훈련 데이터만 |
| 2 | 웹 검색 | 블로그 글, Stack Overflow, 릴리스 노트 |
| 3 | Context7 커넥터 | 공식 Polars 문서 |
| 4 | 둘 다 | 문서 + 웹 — 모델이 하위 주제별로 최적의 소스를 고름 |
| 5 | 필터링된 커넥터 | 단일 문서 검색 도구 (리졸버 건너뜀) |
API 상태: 이 스크립트는 client.beta.connectors, client.beta.agents, client.beta.conversations를 사용해요. 이들은 베타 엔드포인트라 변경될 수 있어요. 최신 API 레퍼런스는 Connectors 문서를 참고하세요.
이 튜토리얼의 Python 버전도 여기에서 볼 수 있어요.
사전 준비 (Prerequisites)
이 쿡북을 완료하려면 다음이 필요해요:
- Node.js와 패키지 매니저 (npm, pnpm, yarn)
- Mistral 계정과 API 키
환경 설정 (Environment setup)
설치 (Install)
다음 방법 중 하나로 Mistral TypeScript SDK와 .env 파일에서 API 키를 불러오는 데 필요한 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 키 섹션으로 이동해 Connector access scope로 Private and shared connectors를 선택하고 새 API 키를 만드세요.
프로젝트 루트에 .env를 만들고 Mistral API 키를 추가하세요:
MISTRAL_API_KEY=your-mistral-api-key
1단계 — 설정 (Setup)
프로젝트 디렉터리에 build-a-quickstart-generator.ts를 만드세요:
touch build-a-quickstart-generator.ts
파일을 열고 클라이언트, Context7 서버 URL, 두 프롬프트, 그리고 출력 텍스트를 추출하는 헬퍼 함수를 추가하세요. 나머지 단계들은 main 함수의 try와 finally 블록을 채워요.
PROMPT는 한 번 정의해 1~4단계에서 재사용해서 비교가 공정하도록 해요. 5단계는 다른, 더 집중된 프롬프트를 사용해 Polars 라이브러리를 미리 지정하므로 커넥터의 리졸버 도구가 필요 없어요.
import "dotenv/config";
import { Mistral } from "@mistralai/mistralai";
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
// Workaround: the Connectors beta API now returns `owner_type` as a string,
// but the SDK's Zod schema still expects a number. This wrapper catches the
// validation error and returns the raw (correctly shaped) response instead.
// Remove this once the SDK ships a fix.
async function createConnector(
params: Parameters<typeof client.beta.connectors.create>[0],
) {
try {
return await client.beta.connectors.create(params);
} catch (e: any) {
if (e.rawValue && e.statusCode === 201) return e.rawValue;
throw e;
}
}
const CONTEXT7_URL = "https://mcp.context7.com/mcp";
const PROMPT =
"Write a Polars quickstart for a developer who knows pandas. " +
"Cover: installation, reading a CSV, filtering rows, groupby aggregation, " +
"and lazy evaluation. End with 3 gotchas when migrating from pandas. " +
"Include runnable code examples.";
const FILTERED_PROMPT =
"Using the Polars documentation, explain lazy evaluation in Polars. " +
"Cover: what LazyFrame is, how to build a lazy query with .lazy(), " +
"how .collect() triggers execution, and when to prefer lazy over eager. " +
"Include a runnable before/after code example.";
// Extract printable text from a conversation output entry.
function outputText(output: any): string {
const content = output.content;
if (typeof content === "string") return content;
return (content as any[]).map((c) => c.text ?? "").join("");
}
async function main(): Promise<void> {
let agentId: string | undefined;
let connectorId: string | undefined;
try {
// Step 2 — Baseline: no tools
// Step 3 — Web search
// Step 4 — Context7 connector: create connector, create agent, stream
// Step 5 — Both tools combined: update agent
// Step 6 — Filtered connector: update agent
} finally {
// Cleanup — delete agent and connector
}
}
main().catch(console.error);
2단계 — 기준선: 도구 없음 (Baseline: no tools)
우선 도구 없이 모델에게 Polars 퀵스타트를 생성하라고 요청해요. 모델은 훈련 데이터만 사용할 수 있으므로, 지식 컷오프 이후에 바뀐 것은 빠지거나 틀릴 수 있어요.
// Step 2 — Baseline: no tools를 다음으로 바꾸세요:
// Step 2 — Baseline: no tools
console.log("--- Step 1: Baseline (no tools) ---\n");
const baseline = await client.beta.conversations.start({
model: "mistral-medium-latest",
inputs: [{ role: "user", content: PROMPT }],
});
for (const output of baseline.outputs ?? []) {
if ((output as any).type === "message.output") {
console.log(outputText(output));
}
}
3단계 — 웹 검색 (Web search)
내장 도구로 web_search를 추가하면 모델이 최신 정보(최근 블로그 글, Stack Overflow 답변, 릴리스 노트)를 가져올 수 있어요. 커넥터 설정은 필요 없어요.
1단계와 출력을 비교해보면 더 최신 문법과 커뮤니티 팁이 보일 거예요.
// Step 3 — Web search를 다음으로 바꾸세요:
// Step 3 — Web search
console.log("\n--- Step 2: Web search ---\n");
const webSearch = await client.beta.conversations.start({
model: "mistral-medium-latest",
inputs: [{ role: "user", content: PROMPT }],
tools: [{ type: "web_search" }],
});
for (const output of webSearch.outputs ?? []) {
if ((output as any).type === "message.output") {
console.log(outputText(output));
}
}
4단계 — Context7 커넥터: 커넥터 생성, 에이전트 생성, 스트리밍
Context7은 널리 쓰이는 오픈소스 라이브러리의 최신 문서를 제공하는 MCP 서버예요. 인증이 필요 없어서 좋은 첫 커넥터로 적합해요.
이 단계는 네 부분으로 나뉘어요:
- Context7의 MCP 엔드포인트를 가리키는 커넥터 생성
- 자격 증명 등록 (Context7은 공개라 비어 있지만 — 레코드는 존재해야 해요)
- 도구를 나열해 커넥터가 무엇을 노출하는지 확인 (6단계에서 이름을 사용할 거예요)
- 커넥터가 연결된 에이전트를 만들어 대화를 스트리밍 — conversations API를 직접 쓰는 대신 에이전트를 쓰는 이유는 커넥터 도구 호출(
tool.execution.started,tool.execution.delta,tool.execution.done)을 실시간으로 볼 수 있기 때문이에요
// Step 4 — Context7 connector: create connector, create agent, stream을 다음으로 바꾸세요:
// Step 4 — Context7 connector: create connector, create agent, stream
console.log("\n--- Step 3: Context7 connector (official docs) ---\n");
const connector = await createConnector({
name: "quickstart_context7",
description: "Context7 connector — library documentation lookup",
server: CONTEXT7_URL,
visibility: "private",
});
connectorId = 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}`);
const toolsList = await client.beta.connectors.listTools({
connectorIdOrName: connector.name!,
});
let docToolName: string | undefined;
console.log(`\nTools exposed by ${connector.name}:`);
for (const tool of toolsList) {
console.log(` - ${tool.name}: ${tool.description}`);
if (
(tool.description ?? "").toLowerCase().includes("documentation") ||
tool.name.toLowerCase().includes("doc")
) {
docToolName = tool.name;
}
}
if (docToolName) {
console.log(`\nDoc-retrieval tool for Step 5: ${docToolName}`);
}
const agent = await client.beta.agents.create({
name: "quickstart_context7_agent",
model: "mistral-medium-latest",
instructions:
"You are a helpful programming assistant. " +
"When asked about a library, always use the Context7 connector to look up " +
"the official documentation before answering. Do not rely on training data alone.",
tools: [{ type: "connector" as const, connectorId: connector.id }],
});
agentId = agent.id;
console.log(`Agent ready: ${agent.name} (id=${agent.id})\n`);
let conversationId: string | undefined;
const stream3 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream3) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages3 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput3 = [...(messages3.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput3) {
console.log("\n");
console.log(outputText(lastOutput3));
}
등록한 Connectors를 Studio에서 확인하세요.
5단계 — 두 도구 결합: 에이전트 업데이트 (Both tools combined: update agent)
이게 핵심이에요. 에이전트에 웹 검색과 Context7 커넥터를 모두 주도록 업데이트해요. 모델은 정확한 API 예제를 위해 공식 문서를, 커뮤니티 지식·마이그레이션 함정·최근 릴리스 노트를 위해 웹 결과를 가져올 수 있어요. 각 하위 주제에 어떤 소스를 쓸지 모델이 결정해요.
1~3단계와 비교해보면 — 결합 버전이 눈에 띄게 풍부해요.
// Step 5 — Both tools combined: update agent를 다음으로 바꾸세요:
// Step 5 — Both tools combined: update agent
console.log("\n--- Step 4: Web search + Context7 connector ---\n");
await client.beta.agents.update({
agentId: agent.id,
updateAgentRequest: {
tools: [
{ type: "web_search" },
{ type: "connector" as const, connectorId: connector.id },
],
},
});
conversationId = undefined;
const stream4 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream4) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages4 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput4 = [...(messages4.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput4) {
console.log("\n");
console.log(outputText(lastOutput4));
}
6단계 — 필터링된 커넥터: 에이전트 업데이트 (Filtered connector: update agent)
Context7은 여러 도구를 노출해요: 리졸버(이름에서 라이브러리 ID를 찾는 역할)와 문서 검색 도구(라이브러리 ID로 페이지를 가져오는 역할). 이미 라이브러리 ID를 안다면, toolConfiguration.include로 커넥터를 문서 검색 도구 하나로 제한해 리졸버를 건너뛸 수 있어요.
마지막으로 에이전트를 업데이트해 그 도구들을 하나의 필터링된 커넥터로 교체해요. 이 단계는 Polars 라이브러리를 미리 지정하는 더 집중된 프롬프트를 사용하므로 리졸버가 불필요하조.
// Step 6 — Filtered connector: update agent를 다음으로 바꾸세요:
// Step 6 — Filtered connector: update agent
if (docToolName) {
console.log(
`\n--- Step 5: Filtered connector (only ${docToolName}) ---\n`,
);
await client.beta.agents.update({
agentId: agent.id,
updateAgentRequest: {
tools: [
{
type: "connector" as const,
connectorId: connector.id,
toolConfiguration: {
include: [docToolName],
},
},
],
},
});
conversationId = undefined;
const stream5 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: FILTERED_PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream5) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages5 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput5 = [...(messages5.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput5) {
console.log("\n");
console.log(outputText(lastOutput5));
}
} else {
console.log(
"\nSkipped Step 5 — DOC_TOOL_NAME was not detected. Set it manually from the tool list in Step 3.",
);
}
정리 (Cleanup)
완료되면 에이전트와 커넥터를 삭제하세요. 3~5단계에서 하나의 에이전트를 재사용해(매번 도구만 업데이트) 정리할 에이전트는 하나뿐이에요.
finally 블록의 // Cleanup — delete agent and connector를 다음으로 바꾸세요:
// Cleanup — delete agent and connector
if (agentId) {
await client.beta.agents.delete({ agentId });
console.log(`\nAgent deleted: ${agentId}`);
}
if (connectorId) {
const result = await client.beta.connectors.delete({
connectorId,
});
console.log(`Connector deleted: ${(result as any).message}`);
}
실행 (Run)
모든 단계를 넣은 뒤 스크립트를 실행하세요:
npx tsx build-a-quickstart-generator.ts
또는 tsx를 dev dependency로 설치했다면 npm start로 실행할 수 있어요.
전체 스크립트 (Complete script)
참고용으로 모든 단계를 합친 전체 스크립트는 다음과 같아요. 전체 프로젝트는 GitHub에서도 볼 수 있어요.
import "dotenv/config";
import { Mistral } from "@mistralai/mistralai";
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const CONTEXT7_URL = "https://mcp.context7.com/mcp";
const PROMPT =
"Write a Polars quickstart for a developer who knows pandas. " +
"Cover: installation, reading a CSV, filtering rows, groupby aggregation, " +
"and lazy evaluation. End with 3 gotchas when migrating from pandas. " +
"Include runnable code examples.";
const FILTERED_PROMPT =
"Using the Polars documentation, explain lazy evaluation in Polars. " +
"Cover: what LazyFrame is, how to build a lazy query with .lazy(), " +
"how .collect() triggers execution, and when to prefer lazy over eager. " +
"Include a runnable before/after code example.";
// Extract printable text from a conversation output entry.
function outputText(output: any): string {
const content = output.content;
if (typeof content === "string") return content;
return (content as any[]).map((c) => c.text ?? "").join("");
}
async function main(): Promise<void> {
let agentId: string | undefined;
let connectorId: string | undefined;
try {
// Step 2 — Baseline: no tools
console.log("--- Step 1: Baseline (no tools) ---\n");
const baseline = await client.beta.conversations.start({
model: "mistral-medium-latest",
inputs: [{ role: "user", content: PROMPT }],
});
for (const output of baseline.outputs ?? []) {
if ((output as any).type === "message.output") {
console.log(outputText(output));
}
}
// Step 3 — Web search
console.log("\n--- Step 2: Web search ---\n");
const webSearch = await client.beta.conversations.start({
model: "mistral-medium-latest",
inputs: [{ role: "user", content: PROMPT }],
tools: [{ type: "web_search" }],
});
for (const output of webSearch.outputs ?? []) {
if ((output as any).type === "message.output") {
console.log(outputText(output));
}
}
// Step 4 — Context7 connector: create connector, create agent, stream
console.log("\n--- Step 3: Context7 connector (official docs) ---\n");
const connector = await createConnector({
name: "quickstart_context7",
description: "Context7 connector — library documentation lookup",
server: CONTEXT7_URL,
visibility: "private",
});
connectorId = 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}`);
const toolsList = await client.beta.connectors.listTools({
connectorIdOrName: connector.name!,
});
let docToolName: string | undefined;
console.log(`\nTools exposed by ${connector.name}:`);
for (const tool of toolsList) {
console.log(` - ${tool.name}: ${tool.description}`);
if (
(tool.description ?? "").toLowerCase().includes("documentation") ||
tool.name.toLowerCase().includes("doc")
) {
docToolName = tool.name;
}
}
if (docToolName) {
console.log(`\nDoc-retrieval tool for Step 5: ${docToolName}`);
}
const agent = await client.beta.agents.create({
name: "quickstart_context7_agent",
model: "mistral-medium-latest",
instructions:
"You are a helpful programming assistant. " +
"When asked about a library, always use the Context7 connector to look up " +
"the official documentation before answering. Do not rely on training data alone.",
tools: [{ type: "connector" as const, connectorId: connector.id }],
});
agentId = agent.id;
console.log(`Agent ready: ${agent.name} (id=${agent.id})\n`);
let conversationId: string | undefined;
const stream3 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream3) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages3 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput3 = [...(messages3.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput3) {
console.log("\n");
console.log(outputText(lastOutput3));
}
// Step 5 — Both tools combined: update agent
console.log("\n--- Step 4: Web search + Context7 connector ---\n");
await client.beta.agents.update({
agentId: agent.id,
updateAgentRequest: {
tools: [
{ type: "web_search" },
{ type: "connector" as const, connectorId: connector.id },
],
},
});
conversationId = undefined;
const stream4 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream4) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages4 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput4 = [...(messages4.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput4) {
console.log("\n");
console.log(outputText(lastOutput4));
}
// Step 6 — Filtered connector: update agent
if (docToolName) {
console.log(
`\n--- Step 5: Filtered connector (only ${docToolName}) ---\n`,
);
await client.beta.agents.update({
agentId: agent.id,
updateAgentRequest: {
tools: [
{
type: "connector" as const,
connectorId: connector.id,
toolConfiguration: {
include: [docToolName],
},
},
],
},
});
conversationId = undefined;
const stream5 = await client.beta.conversations.startStream(
{
agentId: agent.id,
inputs: [{ role: "user", content: FILTERED_PROMPT }],
},
{ timeoutMs: 300_000 },
);
for await (const item of stream5) {
const data = item.data;
const eventType = (data as any).type;
const name = (data as any).name ?? "";
if (eventType === "conversation.response.started") {
conversationId = (data as any).conversationId;
} else if (eventType === "message.output.delta") {
process.stdout.write(".");
} else {
console.log(`\n[${eventType}]${name ? ` ${name}` : ""}`);
}
}
const messages5 = await client.beta.conversations.getMessages({
conversationId: conversationId!,
});
const lastOutput5 = [...(messages5.messages ?? [])]
.reverse()
.find((m) => (m as any).type === "message.output") as any;
if (lastOutput5) {
console.log("\n");
console.log(outputText(lastOutput5));
}
} else {
console.log(
"\nSkipped Step 5 — DOC_TOOL_NAME was not detected. Set it manually from the tool list in Step 3.",
);
}
} finally {
// Cleanup — delete agent and connector
if (agentId) {
await client.beta.agents.delete({ agentId });
console.log(`\nAgent deleted: ${agentId}`);
}
if (connectorId) {
const result = await client.beta.connectors.delete({
connectorId,
});
console.log(`Connector deleted: ${(result as any).message}`);
}
}
}
main().catch(console.error);
요약 (Summary)
이 스크립트는 Mistral 대화에 도구를 추가할수록 출력 품질이 어떻게 점진적으로 좋아지는지 보여줘요 — 훈련 데이터만 쓰는 기준선에서부터, 웹 검색과 문서 커넥터를 거쳐, 가장 풍부한 결과를 내는 결합까지.
만든 것:
- 도구를 추가할 때마다 개선되는 Polars 퀵스타트 생성기
- 공식 라이브러리 문서를 가져오는 Context7 커넥터
- 불필요한 커넥터 도구를 건너뛰는 필터링된 도구 구성
사용한 Mistral 기능:
- 커넥터 (베타)
- Conversations API (베타)
- Agents API (베타) — 도구 실행 이벤트 스트리밍에 사용
- 웹 검색 내장 도구
- 도구 필터링 (
toolConfiguration.include)
기타 서비스:
- Context7 — 오픈소스 라이브러리 문서용 MCP 서버
더 알아보기 (Learn more)
- Connectors 문서 — 커넥터 개념과 API 레퍼런스
- Mistral Agents API — 도구 실행 이벤트 스트리밍을 포함한 에이전트
- Context7 — 오픈소스 라이브러리 문서용 MCP 서버
- Python 버전 퀵스타트 생성기 튜토리얼