커넥터와 웹 검색으로 퀵스타트 생성기 만들기
커넥터와 웹 검색으로 퀵스타트 생성기 만들기 (TypeScript) (Build a Quickstart Generator with Connectors and Web Search, TypeScript)
Polars(Python용 빠른 DataFrame 라이브러리)를 배우고 싶다고 가정해 볼게요. 직접 문서를 읽고 블로그 포스트를 훑어보며 퀵스타트를 조립할 수도 있지만, Mistral에게 적절한 도구 접근권을 주고 대신 처리하게 할 수도 있어요. 이 쿡북은 같은 프롬프트를 다섯 번 보내면서 각각 다른 도구 구성을 사용해, 모델에 더 나은 소스를 줄수록 출력 품질이 어떻게 향상되는지 보여주는 TypeScript 버전이에요.
출처: 문서
본문
Polars(Python용 빠른 DataFrame 라이브러리)를 배우고 싶다고 가정해 볼게요. 문서를 읽고, 블로그 포스트를 훑어보고, 퀵스타트를 직접 조립할 수도 있지만 — 올바른 도구에 접근할 수 있는 Mistral에게 맡길 수도 있어요.
이 스크립트는 동일한 프롬프트를 다섯 번 보내되, 각각 다른 도구 구성을 사용해요. 모델에 더 나은 소스를 줄수록 출력 품질이 어떻게 향상되는지 볼 수 있어요.
| Step | Tools | What the model can access |
|---|---|---|
| 1 | None | Training data only |
| 2 | Web search | Blog posts, Stack Overflow, release notes |
| 3 | Context7 connector | Official Polars documentation |
| 4 | Both | Docs + web — the model picks the best source per sub-topic |
| 5 | Filtered connector | A single doc-retrieval tool (skip the resolver) |
API 상태 (API status): 이 스크립트는 client.beta.connectors, client.beta.agents, client.beta.conversations를 사용해요. 이들은 베타(beta) 엔드포인트로 변경될 수 있어요. 최신 API 참조는 Connectors 문서를 참조하세요.
같은 튜토리얼의 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-quickstart-generator.ts를 만들어요.
touch build-a-quickstart-generator.ts
파일을 열고 클라이언트, Context7 서버 URL, 두 프롬프트, 출력 텍스트를 추출하는 헬퍼 함수를 추가해요. 남은 단계들은 main 함수의 try와 finally 블록을 채워 나가요.
공정한 비교를 위해 PROMPT를 한 번 정의하고 1~4단계에서 재사용해요. 5단계는 Polars 라이브러리를 미리 지정해서 커넥터의 리졸버(resolver) 도구가 불필요하도록 하는, 더 집중된 다른 프롬프트를 사용해요.
import "dotenv/config";
import { Mistral } from "@mistralai/mistralai";
const client = new Mistral({ apiKey: proces..._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단계 — 기준선: 도구 없음 (Step 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단계 — 웹 검색 (Step 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 커넥터: 커넥터 생성, 에이전트 생성, 스트림 (Step 4 — Context7 connector: create connector, create agent, stream)
Context7는 인기 오픈소스 라이브러리의 최신 문서를 제공하는 MCP 서버예요. 인증이 필요 없어서 첫 커넥터로 좋아요.
이 단계는 네 부분으로 나뉘어요.
- Create: Context7의 MCP 엔드포인트를 가리키는 커넥터를 만들어요.
- Register credentials: 자격 증명을 등록해요(Context7은 공개 서비스라 비어있지만, 레코드는 존재해야 해요).
- List tools: 커넥터가 노출하는 도구를 확인해요 (6단계에서 이 이름들을 사용할 거예요).
- Create an agent: 커넥터가 연결된 에이전트를 만들고 대화를 스트리밍해요 — 커넥터 도구 호출(
tool.execution.started,tool.execution.delta,tool.execution.done)을 실시간으로 보기 위해 conversations API 대신 에이전트를 사용해요.
// 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));
}
등록된 Connector를 Studio에서 확인할 수 있어요.
5단계 — 두 도구 결합: 에이전트 업데이트 (Step 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단계 — 필터링된 커넥터: 에이전트 업데이트 (Step 6 — Filtered connector: update agent)
Context7은 여러 도구를 노출해요: 라이브러리 ID를 이름에서 찾는 리졸버(resolver)와 라이브러리 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: proces..._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 대화에 도구를 추가하면 출력 품질이 점진적으로 어떻게 향상되는지 보여줬어요. 학습 데이터만 사용하는 기준선 응답부터, 웹 검색과 문서 커넥터를 거쳐, 둘을 결합해 가장 풍부한 결과를 얻기까지요.
만든 것 (What you built):
- 도구를 추가할 때마다 개선되는 Polars 퀵스타트 생성기
- 공식 라이브러리 문서를 가져오는 Context7 커넥터
- 불필요한 커넥터 도구를 건너뛰는 필터링된 도구 구성
사용한 Mistral 기능 (Mistral features used):
- Connectors (beta)
- Conversations API (beta)
- Agents API (beta) — 도구 실행 이벤트 스트리밍에 사용
- Web search 내장 도구
- Tool filtering (
toolConfiguration.include)
기타 서비스 (Other services):
- Context7 — 오픈소스 라이브러리 문서용 MCP 서버
Connector를 Studio에서 확인할 수 있어요.