Open4

MCP + Agent , Bun + React UI作成

knaka Tech-Blogknaka Tech-Blog

概要

  • MCP等の、Bun + React 構成 メモです。

[ 公開 2025/08/11 ]


環境

  • bun:1.2.20
  • React
  • Vercel AI SDK

関連


書いたコード


  • dev-start
bun run build
bun run dev

  • front-UI
  • mcp_react4/src/client/Home.tsx
  • chat-start

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp_react4/src/client/Home.tsx

  const chatStart = async function(){
    try{    
      setText("");
      setIsLoading(false);
      const elem = document.getElementById("input_text") as HTMLInputElement;
      let inText = "";
      if(elem){
        inText = elem.value;
      };
      console.log("inText=", inText);
      if(!inText){ return; }
      setIsLoading(true);
      const agentName = AgentUtil.validAgentName(inText.trim());
      console.log("agentName=", AgentUtil.validAgentName(inText.trim()));
      if(agentName){
        let items = [];
        if(agentName === "first-agent"){
          items = firstAgent();
        }
        if(agentName === "price-list-up-agent"){
          items = PriceListUpAgent(inText);
        }
        if(agentName === "price-list-updown-agent"){
          items = PriceListUpdownAgent(inText);
        }
        console.log(items);

        let htmAll = "";
        const agentSendProc = async function(){
          for(const row of items) {
            htmAll += `<div class="label character-label w-full">${row.title}</div>`;
            console.log(row);
            let res = await ApiUtil.post({messages: row.text }, "/api/chat");
            console.log(res);
            htmAll += `<div class="chat-bubble character-bubble w-full">`;
            htmAll += marked.parse(res.text);
            htmAll +=  `</div>`
            //console.log(htmAll);
            setText(htmAll);
          };
          setIsLoading(false);
        }
        agentSendProc();
        return;
      }
      const item = {messages: inText};
      const body: any = JSON.stringify(item);		
      const res = await fetch("/api/chat", {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},      
        body: body
      });
      setIsLoading(false);
      const json = await res.json();
      console.log(json);
      let htm = marked.parse(json.text);
      console.log(htm);
      //@ts-ignore
      setText(htm);
    } catch(e){
      console.error(e);
    }
  }


  • LLM 送信、エンドポイント
  • mcp_react4/src/index.ts

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp_react4/src/index.ts

app.post('/api/chat', async (req: any, res: any) => {
  try {
    const body = req.body;
    console.log(body)

    const result = await generateText({
      model: google(MODEL_NAME),
      tools: {
        firstGetRandom, firstGetDate, firstGetTime
      },
      maxSteps: 5,
      messages: [{ role: "user", content: body.messages }],
    });
    console.log("artifact:");
    console.log(result.text);

    res.send({ret: 200, text: result.text});
  } catch (error) {
    res.sendStatus(500);
  }
});

knaka Tech-Blogknaka Tech-Blog

Agent Development Kit , Bun + React + UI

  • Goocle ADK API連携で。Bun + React UI構成

環境

  • bun:1.2.20
  • React
  • Agent Development Kit
  • python

関連


書いたコード


  • dev-start
bun run build
bun run dev

  • .env
ADK_API_URL="http://localhost:8000"

  • front-UI
  • mcp_react6/src/client/Home.tsx
  • chat-start

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp_react6/src/client/Home.tsx

  const chatStart = async function(){
    try{    
      setText("");
      setIsLoading(false);
      const elem = document.getElementById("input_text") as HTMLInputElement;
      let inText = "";
      let inToolText = "";
      if(elem){
        inText = elem.value;
      };
      const elemToolText = document.getElementById("tool_text") as HTMLInputElement;
      if(elemToolText){
        inToolText = elemToolText.value;
      }

      console.log("inText=", inText);
      console.log("inToolText=", inToolText);
      if(!inText){ return; }
      setIsLoading(true);
      const agentName = AgentUtil.validAgentName(inText.trim());
      console.log("agentName=", agentName);
      if(agentName){
        let items = [];
        if(agentName === "tool_agent_3"){
          items = toolAgent3();
        }
        console.log(items);

        let htmAll = "";
        let itemCount = 1;
        const agentSendProc = async function(){
          for(const row of items) {
            htmAll += `<div class="label character-label w-full">${row.title}</div>`;
            console.log(row);
            let res = null;
            if(itemCount === 2){
              res = await AgentUtil.postAgent(inToolText, "tool_agent_3");
            }else{
              res = await AgentUtil.postAgent(row.text, "tool_agent_3");
            }
            itemCount += 1; 
            console.log(res);
            htmAll += `<div class="chat-bubble character-bubble w-full">`;
            htmAll += marked.parse(res.text);
            htmAll +=  `</div>`
            //console.log(htmAll);
            setText(htmAll);
          };
          setIsLoading(false);
        }
        agentSendProc();
        return;
      }
    } catch(e){
      console.error(e);
    }
  }


  • chat-start エンドポイント
  • mcp_react6/src/index.ts

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp_react6/src/index.ts

app.post('/api/chat', async (req: any, res: any) => {
  try {
    const body = req.body;
    console.log(body)
    const item = {
      appName: body.appName,
      userId: body.userId,
      sessionId: body.sessionId,
      newMessage: {
        role: "user",
        parts: [{
          text: body.messages 
        }]
      }
    };
    console.log(item)
    const sendBody = JSON.stringify(item);	
    const url = process.env.ADK_API_URL;	
    console.log("url=", url)
    const response = await fetch( url + "/run", {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},      
      body: sendBody
    });
    if(response.ok === false){
      console.error("Error, res.ok = NG");
      throw new Error("Error, res.ok = NG");
    }
    const json = await response.json();    
    const outIndex = json.length - 1;
    console.log("#outIndex=", outIndex);
    console.log("artifact:");
    if(json[outIndex]){
      const target = json[outIndex].content;
      console.log(target.parts);
      if(target.parts[0]){
        console.log("text=", target.parts[0].text);
        return res.send({ret: 200, text: target.parts[0].text});
      }else{
        console.log("text= NULL");
      }
    }
    res.send({ret: 200, text: ""});
  } catch (error) {
    res.sendStatus(500);
  }
});

knaka Tech-Blogknaka Tech-Blog

Ollama + qwen3:8b + SQLite, Bun + MCP CLI版

  • LLM= Ollama + qwen3:8b ローカルPC
  • MCPで、SQLite書込、検索機能

環境

  • bun:1.2.20
  • React
  • LLM Ollama + qwen3:8b
  • SQLite
  • drizzle-orm

書いたコード

https://orm.drizzle.team/docs/get-started/sqlite-new


  • dev-start
bun run build
bun run dev

  • .env
DB_FILE_NAME=file:local.db

  • schema
  • mcp-cli-16/src/db/schema.ts

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp-cli-16/src/db/schema.ts

import { int, sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";
import { sql } from 'drizzle-orm';

export const items = sqliteTable('items', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  created_at: text('created_at').notNull().default(new Date().toISOString()),
  updated_at: text('updated_at').notNull().default(new Date().toISOString())
});

export type Item = typeof items.$inferSelect;
export type NewItem = typeof items.$inferInsert;

  • TODO 登録
  • mcp-cli-16/src/tools/addTodo.ts

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp-cli-16/src/tools/addTodo.ts

import { generateText, tool } from "ai";
import { z } from "zod";
import { drizzle } from 'drizzle-orm/libsql';
import { eq } from 'drizzle-orm';
import { items } from '../db/schema';

export const addTodo = tool({
  description: "指定した 文字列をデータベースに登録する。",
  // ツールを呼び出すパラメータ
  parameters: z.object({
    text: z.string().min(1, { message: 'タイトルは必須です' })
  }),
  execute: async ({ text }) => {
    const db = drizzle(process.env.DB_FILE_NAME);
    const newItem = {
      title: text,
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
    };
    const result = await db.insert(items).values(newItem).returning();
    return "result : " + text;
  },
});


  • Log
mcp-cli-16>npm run dev

> dev
> tsx src

input:TODO一覧を、表形式で表示します。
input= TODO一覧を、表形式で表示します。
artifact:
<think>

</think>

| ID | タイトル       | 作成日時           | 更新日時           |
|----|----------------|--------------------|--------------------|
| 2  | 株式の勉強する | 2025-08-14T05:32:55.366Z | 2025-08-14T05:32:55.366Z |
| 3  | 公園に散歩    | 2025-08-14T05:42:40.337Z | 2025-08-14T05:42:40.337Z |
knaka Tech-Blogknaka Tech-Blog

Ollama + qwen3:8b + SQLite, Bun + MCP React UI

  • LLM= Ollama + qwen3:8b ローカルPC
  • MCPで、SQLite書込、検索機能
  • Bun + React UI 構成
  • 前のCLI版と、機能面は似ています。


環境

  • bun:1.2.20
  • React
  • LLM Ollama + qwen3:8b
  • SQLite
  • drizzle-orm

書いたコード

https://orm.drizzle.team/docs/get-started/sqlite-new


  • dev-start
bun run build
bun run dev

  • .env
DB_FILE_NAME=file:local.db

  • schema
  • mcp_react8/src/db/schema.ts
import { int, sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";
import { sql } from 'drizzle-orm';

export const items = sqliteTable('items', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  created_at: text('created_at').notNull().default(new Date().toISOString()),
  updated_at: text('updated_at').notNull().default(new Date().toISOString())
});

export type Item = typeof items.$inferSelect;
export type NewItem = typeof items.$inferInsert;

  • TODO 登録
  • mcp_react8/src/tools/addTodo.ts

https://github.com/kuc-arc-f/bun_41ex/blob/main/mcp_react8/src/tools/addTodo.ts


 const chatStart = async function(){
    try{    
      setText("");
      setIsLoading(false);
      const elem = document.getElementById("input_text") as HTMLInputElement;
      let inText = "";
      if(elem){
        inText = elem.value;
      };
      const inputTodo = document.getElementById("input_todo") as HTMLInputElement;
      let inTextTodo = "";
      if(inputTodo){
        inTextTodo = inputTodo.value;
      };

      console.log("inText=", inText);
      if(!inText){ return; }
      setIsLoading(true);
      //const agentName = AgentUtil.validAgentName(inText.trim());
      const agentName = inText.trim();
      console.log("agentName=", agentName);
      if(agentName){
        let items = [];
        if(agentName === "first-agent"){
          items = firstAgent();
        }
        if(agentName === "todo-agent"){
          items = todoAgent();
        }
        console.log(items);

        let htmAll = "";
        let countTool = 1;
        const agentSendProc = async function(){
          for(const row of items) {
            htmAll += `<div class="label character-label w-full">${row.title}</div>`;
            console.log(row);
            let res = "";
            if(agentName === "todo-agent" && countTool === 2)
            {
              res = await ApiUtil.post({messages: inTextTodo }, "/api/chat");
            }else{
              res = await ApiUtil.post({messages: row.text }, "/api/chat");
            }
            console.log(res);
            htmAll += `<div class="chat-bubble character-bubble w-full">`;
            htmAll += marked.parse(res.text);
            htmAll +=  `</div>`
            //console.log(htmAll);
            setText(htmAll);
            countTool += 1;
          };
          setIsLoading(false);
        }
        agentSendProc();
        return;
      }
      const item = {messages: inText};
      const body: any = JSON.stringify(item);		
      const res = await fetch("/api/chat", {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},      
        body: body
      });
      setIsLoading(false);
      const json = await res.json();
      console.log(json);
      let htm = marked.parse(json.text);
      console.log(htm);
      //@ts-ignore
      setText(htm);
    } catch(e){
      console.error(e);
    }
  }