先日、「百年の孤独」を買い直した。ガルシア・マルケスがノーベル賞を取ったときに買っては見たものの暫く放置の状態で、引っ越しの折にBOOK OFF に売ってしまったような覚えがある。引っ越すときに、3,000冊の本を2,000冊位に減らした時にまぎれてしまった訳だが。あまり本は捨てるものではない。とはいえ、最近は電子書籍が便利になったので、Kindleに2,000冊ほど入っている(漫画も含めてだけど)。物理本は1,000冊弱にはなったのじゃないだろうか。で、買い直したのは、先日「百年の孤独」の100分の解説だったかで、大江健三郎や筒井康隆に影響を与えたとことを知ったのである。実は「百年の孤独」は1967年の出版で、私が生まれる前だ。ああ、そんなに古い作品であったのか、というのと大江健三郎(たぶん、万延元年のフットボールか燃え上がる木あたり)に影響を与えたとか、筒井康隆(たぶん、バブリング創成期とか)に影響を与えたとかという話になると、もう一回読んでみないとあかんだろうということになる。そうなると井上ひさしの「吉里吉里人」もそうじゃないだろうか、と思う訳で。で、まだ、「百年の孤独」は読んでいないのだが。
筒井康隆の初期作品に「東海道戦争」(1965年)がある。戦争を眺めるというパターンなのだけど、「銀河鉄道999」(1977年)にも戦争を眺めて食事をするシーンがある。松本零士は筒井康隆の5つぐらい下になるが、ほぼ同世代といってよいだろう。筒井康隆が90歳で存命なわけだが、星新一、小松左京、藤子不二雄、手塚治虫、とこのあたりの SF 世代はみな鬼籍に入っていしまっている。まあ、そういう意味では、夢野久作とか沼正三とか押川春浪なんかは、さらに年上なのだけど。そういう SF の技法に沿った形で描けるのは筒井康隆しか残っていないと言える。
ただし、「朝のガスパール」や「ビアンカ・オーバースタディ」のような書き方、さらに「モナドの領域」が現在の SF 小説(ラノベも含めて)にない訳ではない。むしろ、異世界転生ものだったり、ゲーム世界の拡張であったりするストーリーはそういう「メタ」的な視点が多いに含まれているし、それを知っていることが読者/視聴者の前提知識であったりする。例えば、涼宮ハルヒシリーズであったり、Reゼロであったり、SSSS.GRIDMANであったり、このすばであったり。主人公は意図的にメタ世界を意識している。もちろん、特別な能力があったりなかったりするわけだが、そういう現実的な世界とは別の世界に居ることを前知識として知っているという仮想世界、という読者との申し合わせは済んでいる状態になる。そのあたりは、「朝のガスパール」の連載時点でも、漫画の中に作者が登場したり(Dr.スランプとか、ブラック・ジャックとか)するシーンもあり、駒からはみ出したりするシーンがある。もちろん、そこを小説/漫画内に組み込み始めたは、果たして「朝のガスパール」だったのか、それともそれ以前に漫画があったのかは定かではない。
第7章 タイムボックス 第6章のコミュニケーションに関わるものとして、締め切りを明確にした「タイムボックス」制を使います。つまり、いつまで作ればよいか?を明確にして、その間にあれこれと言わないということです。皆さん、プログラマはプロなんですから、途中でサボったりしませんよね、ということとサボっても締め切りには間に合わせてね、ってことです。途中経過は問わないのです。結果が重要です、という話ですね。 逆に言えば、未知なる要素が多いとこきはタイムボックスが有効に働きません。経験上、とあるコードが何時間、何日で作れるのかというのは「勘」を働かせることもありますが、過去の経験との比較をするのがベターです。あと、自分の実力を知るために PSP をしておくとよいです。うまくいかなそうなときは、いったんタイムボックスを外して、調査時間を決めて(時間は無限ではないので)、どのくらいで調べられそうかをチェックします。これは AI エージェントを使ったコーディングでも同じことが言えます。途中で迷路に入ってしまった AI エージェントを “時間” を区切って引き戻します。前回のプロンプトを無しにして、もう一度やり直しをすれば ok です。
CommandType Name Version Source ----------- ---- ------- ------ Application fake.exe 6.1.0.0 C:\Users\masuda\.dotnet\tools\fake.exe
バージョンはこんな感じです。
PS H:\kitami\FolkBearsGroup\folkbears-android\FolkBears> fake --version FAKE 6 - F# Make (6.1.3.0) (running on .NETCoreApp,Version=v6.0) (this line is written to standard error, see https://github.com/fsharp/FAKE/issues/2066) FakePath: C:\Users\masuda\.dotnet\tools\.store\fake-cli\6.1.3\fake-cli\6.1.3\tools\net6.0\any\Fake.Runtime.dll Paket.Core: 8.1.0
#r "paket:
nuget Fake.Core.Target
nuget Fake.Tools.Shell //"
#load "./.fake/build.fsx/intellisense.fsx"
open Fake.Core
Target.create "Clean" (fun _ ->
printfn "Cleaning..."
)
Target.create "Build" (fun _ ->
printfn "Building..."
// gradlew でビルド
Shell.Exec("./gradlew.bat", "assembleDebug") |> ignore
// Shell.Exec("gradlew", "build")
)
Target.create "Default" (fun _ ->
printfn "Default target実行中..."
printfn "gradlewでAndroidアプリをビルドします"
// gradlew でビルド
let result = Shell.Exec("./gradlew.bat", "assembleDebug")
if result = 0 then
printfn "ビルド成功"
else
printfn "ビルド失敗"
failwith "gradlewビルドが失敗しました"
)
// Android 実機にデプロイする
Target.create "Deploy" (fun _ ->
printfn "Deploying to Android device..."
Shell.Exec("./gradlew.bat", "installDevDebug") |> ignore
)
// 特定のデバイス(25041JEGR04165)にデプロイする
Target.create "DeployToDevice" (fun _ ->
printfn "Deploying to device 25041JEGR04165..."
// 環境変数でデバイスを指定
System.Environment.SetEnvironmentVariable("ANDROID_SERIAL", "25041JEGR04165")
let result = Shell.Exec("./gradlew.bat", "installDevDebug")
if result = 0 then
printfn "デプロイ成功 (25041JEGR04165)"
printfn "アプリを起動しています..."
// アプリを起動
let launchResult = Shell.Exec("adb", "-s 25041JEGR04165 shell am start -n jp.mamori_i.app.dev/jp.mamori_i.app.screen.start.SplashActivity")
if launchResult = 0 then
printfn "アプリ起動成功"
else
printfn "アプリ起動失敗"
else
printfn "デプロイ失敗"
failwith "デプロイが失敗しました"
)
// アプリを起動する(デプロイ済みの場合)
Target.create "LaunchApp" (fun _ ->
printfn "Starting app on device 25041JEGR04165..."
let result = Shell.Exec("adb", "-s 25041JEGR04165 shell am start -n jp.mamori_i.app.dev/jp.mamori_i.app.screen.start.SplashActivity")
if result = 0 then
printfn "アプリ起動成功"
else
printfn "アプリ起動失敗"
failwith "アプリ起動が失敗しました"
)
open Fake.Core.TargetOperators
// 依存関係を削除して、Defaultが単独で実行されるようにする
Target.runOrDefault "Default"
色々と書いてありますが、これは vscode 上で GitHub Copilot + Claude Sonnet を使って書き足したものです。文法はは F# なので、既存の生成 AI のモデルに学習結果が含まれています。このために、Fake の書き方も的確に書いてくれます。
vscode の拡張には、Ionide.Ionide-fsharp を入れておくと便利です。
ビルド&実機へのデプロイを試してみましょう。
fake build -t DeployToDevice
無事、ビルドとデプロイが終わると次のところまで表示がでます。
警告が出ていますが、無視して大丈夫です。
Warning: Paket resolved a FSharp.Core with version '9.0.0.0', but fake runs with a version of '8.0.0.0'. This is not supported. Please either lock the version via 'nuget FSharp.Core <nuget-version>' or upgrade fake. Read https://github.com/fsharp/FAKE/issues/2001 for details.
予算が潤沢であれば、中小のIT業者に依頼をかけて1000万円以上で見積もりを掛けることも可能なのですが、実際はそうはいきません。せいぜい数百万、場合によっては100万以下のシステム開発からスタートするところでしょう。一方で、非ITとITをつなげるマッチングサイトもいくつかあるのですが、価格が安すぎるのが難点です。受注をする IT サイドから言えば、100万以下の案件はなかなか動きづらいところがあります(営業活動や見積もり作業も込みとなるので)。まして、10数万円ていどではITアルバイトのようなものです。実際に、小規模の作業であれば、ITアルバイト感覚で受注することもよいのですが、実はそれだけだと採算が合いません。
個人開発者であれば、身がひとつなのですから、Aプロジェクトのタスクをやっている間は、BプロジェクトやCプロジェクトのタスクはお休みなります。一見、マルチタスクで動いているようには見えますが(今後 AI エージェントの活用次第ではそれができるかもしれませんが)、個人の時間軸から言えばシーケンシャルにこなしているだけです。
大抵の場合、作業時間 x 単価ということになるので、AプロジェクトはBプロジェクトの2倍の予算が掛かります、とお客に説明するわけですが、果たしてそうなるでしょうか?当然のことながら、A,B,C のプロジェクトのお客さんは別々で、互いに顔を合わせることはありません。場合によっては、開発者が自分のプロジェクト(Aプロジェクトとか)に専任していると思っているかもしれません。そうです、いわゆる「官庁プロジェクトの開発者は3倍働く」わけですが、それはまた別の話。
- フロントエンド: React - データベース: SQLite - LLM: Local LLM + Next.js - API: OpenAI API + Laravel
こんな風にユースケースを書いておきます。例をいくつか書いておいて、こういうプロンプトを入れたらこういう形で AI が返してくれる。再びチャットを続ける、という形でユーザーインターフェースをデザイン=設計していきます。
以前ならば、このユースケースを設計書に落とし込んで、ルールベースに直して、実装してという手順になり、それを自分でやらなくてはならないのですが、AI エージェントのおかげでこのあたりの手順を AI に任せることができます。漠然としたユースケースなので自動化という訳にはいきませんが、以前よりも実装スピード=プロトタイプが出来るスピードが早いです。
ユースケースを示しながら、設計を AI と相談する
ユースケースが実験できるような、プロトタイプの画面を AI に作成して貰う
設計に従って、AI にコーディングしてもらう
細かい動きを AI に修正してもらう
という形で、ほぼほぼ AI がコーディングを担当していきます。私は、動作確認をしたり、プロトタイプを見て感想を言ったり、テストするポイントを AI に伝えたりという役目です。
AI コーディングには Claude Sonnet を使っていますが、こんな風なイテレーション開発の場合にはこれで十分です。
国語の読書感想文を4チケットに分けて、それぞれの作業内容を書き出してくれます。これは、チケット駆動のチケットの書き出しにあたります。チケット駆動の場合は、プロジェクトが進む中でチケットを書き出すことが多いのですが、夏休みの課題の場合は、全体の総量がわかっているので、このようにチケットを分割してしまったほうが早いです。全体を分割するときに、以前までは付箋を使って計画を立てていたのですが、最近ならば AI を使えば便利です。
ただし、この標準入出力型には難点があって、ストリーム形式なのでローカルな MCP Server を建てる必要が(たぶん)あります。レスポンス&リクエストが一連のストリームになっているので、エラーを返すことができません。適当にログを出力する必要がでてきます。お手軽ではあるのですが、あまり実用的ではないかもしれません。
最初に標準入出力型を実装してみたのですが、あまりうまく拡張できないので HTTP API 型の kkc-mcp-server-api で実装しなおしています。
using System.Text.Json;
using KkcMcpServerApi.Models;
using KkcMcpServerApi.Services;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// ロギング設定
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Information);
// サービス登録
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "KKC MCP Server API",
Version = "v1",
Description = "Model Context Protocol (MCP) サーバーAPI - kkc-webapi のプロキシとして動作します"
});
});
// HttpClient設定
builder.Services.AddHttpClient<IWebApiClient, WebApiClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:8000");
client.Timeout = TimeSpan.FromSeconds(30);
});
// MCPツールプロバイダー登録
builder.Services.AddScoped<McpToolProvider>();
// CORS設定
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors();
// MCP over HTTP のルートエンドポイント
app.MapPost("/", async (HttpContext context, McpToolProvider toolProvider) =>
{
try
{
using var reader = new StreamReader(context.Request.Body);
var jsonRequest = await reader.ReadToEndAsync();
var requestDoc = JsonDocument.Parse(jsonRequest);
var root = requestDoc.RootElement;
if (!root.TryGetProperty("method", out var methodElement))
{
return Results.BadRequest(new { error = "Missing method property" });
}
var method = methodElement.GetString();
var id = root.TryGetProperty("id", out var idElement) ? idElement.GetString() : null;
object? response = method switch
{
"initialize" => new
{
jsonrpc = "2.0",
id = id,
result = new
{
protocolVersion = "2024-11-05",
capabilities = new
{
tools = new { }
},
serverInfo = new
{
name = "kkc-mcp-server-api",
version = "1.0.0"
}
}
},
"tools/list" => new
{
jsonrpc = "2.0",
id = id,
result = new
{
tools = new object[]
{
new
{
name = "search_reservations",
description = "予約を検索します(ID指定)",
inputSchema = new
{
type = "object",
properties = new
{
storeId = new { type = "integer", description = "店舗ID" },
reservationDate = new { type = "string", description = "予約日 (YYYY-MM-DD)" },
customerName = new { type = "string", description = "顧客名" },
limit = new { type = "integer", description = "取得件数", @default = 10 }
}
}
},
new
{
name = "search_reservations_by_name",
description = "予約を検索します(名前指定)",
inputSchema = new
{
type = "object",
properties = new
{
store = new { type = "string", description = "店舗名" },
areaGroup = new { type = "string", description = "エリアグループ名" },
customerName = new { type = "string", description = "顧客名" },
reservationDate = new { type = "string", description = "予約日 (YYYY-MM-DD)" },
limit = new { type = "integer", description = "取得件数", @default = 10 }
}
}
},
new
{
name = "get_calendar",
description = "カレンダー情報を取得します",
inputSchema = new
{
type = "object",
properties = new
{
date = new { type = "string", description = "日付 (YYYY-MM-DD)" },
areaGroupId = new { type = "integer", description = "エリアグループID" }
},
required = new[] { "date", "areaGroupId" }
}
}
}
}
},
"tools/call" => await HandleToolCall(root, toolProvider, id),
_ => new
{
jsonrpc = "2.0",
id = id,
error = new
{
code = -32601,
message = $"Method not found: {method}"
}
}
};
return Results.Json(response);
}
catch (Exception ex)
{
return Results.Json(new
{
jsonrpc = "2.0",
error = new
{
code = -32603,
message = "Internal error",
data = ex.Message
}
});
}
});
async Task<object> HandleToolCall(JsonElement root, McpToolProvider toolProvider, string? id)
{
try
{
if (!root.TryGetProperty("params", out var paramsElement))
{
return new
{
jsonrpc = "2.0",
id = id,
error = new { code = -32602, message = "Missing params" }
};
}
if (!paramsElement.TryGetProperty("name", out var nameElement))
{
return new
{
jsonrpc = "2.0",
id = id,
error = new { code = -32602, message = "Missing tool name" }
};
}
var toolName = nameElement.GetString();
var arguments = paramsElement.TryGetProperty("arguments", out var argsElement)
? argsElement.GetRawText()
: "{}";
var result = toolName switch
{
"search_reservations" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
"search_reservations_by_name" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
"get_calendar" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
_ => throw new ArgumentException($"Unknown tool: {toolName}")
};
return new
{
jsonrpc = "2.0",
id = id,
result = new
{
content = result.Content.Select(c => new
{
type = c.Type,
text = c.Text
}).ToArray()
}
};
}
catch (Exception ex)
{
return new
{
jsonrpc = "2.0",
id = id,
error = new
{
code = -32603,
message = "Tool execution failed",
data = ex.Message
}
};
}
}
// 既存のエンドポイントも保持(開発・テスト用)
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
app.MapGet("/info", () => Results.Ok(new {
name = "KKC MCP Server API",
version = "1.0.0",
description = "ASP.NET Core Minimal API - MCP Server",
endpoints = new[] { "/", "/health", "/info" }
}));
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.Run();
McpToolProvider.cs
using System.Text.Json;
using KkcMcpServerApi.Models;
namespace KkcMcpServerApi.Services;
/// <summary>
/// MCPツールプロバイダー
/// </summary>
public class McpToolProvider
{
private readonly IWebApiClient _webApiClient;
private readonly ILogger<McpToolProvider> _logger;
public McpToolProvider(IWebApiClient webApiClient, ILogger<McpToolProvider> logger)
{
_webApiClient = webApiClient;
_logger = logger;
}
/// <summary>
/// 利用可能なツールの一覧を取得
/// </summary>
public ToolsListResult GetToolsList()
{
var tools = new List<ToolInfo>
{
new ToolInfo
{
Name = "search_reservations",
Description = "予約を検索します。複数の条件で絞り込み検索が可能です。",
InputSchema = new
{
type = "object",
properties = new
{
customerName = new { type = "string", description = "顧客名での検索" },
customerNameKana = new { type = "string", description = "顧客名カナでの検索" },
phoneNumber = new { type = "string", description = "電話番号での検索" },
email = new { type = "string", description = "メールアドレスでの検索" },
receptionDate = new { type = "string", description = "受付日での検索(YYYY-MM-DD形式)" },
areaGroupId = new { type = "integer", description = "エリアグループIDでの検索" },
storeId = new { type = "integer", description = "店舗IDでの検索" },
reservationDate = new { type = "string", description = "予約日での検索(YYYY-MM-DD形式)" },
timeZone = new { type = "string", description = "時間帯での検索" },
status = new { type = "string", description = "予約ステータスでの検索" },
page = new { type = "integer", description = "ページ番号(デフォルト: 1)" },
perPage = new { type = "integer", description = "1ページあたりの件数(デフォルト: 10)" }
}
}
},
new ToolInfo
{
Name = "search_reservations_by_name",
Description = "販売会社名、店舗名、エリアグループ名などの名前を使用して予約を検索します。",
InputSchema = new
{
type = "object",
properties = new
{
sellingCompany = new { type = "string", description = "販売会社名での検索" },
store = new { type = "string", description = "店舗名での検索" },
areaGroup = new { type = "string", description = "エリアグループ名での検索" },
customerName = new { type = "string", description = "顧客名での検索" },
phoneNumber = new { type = "string", description = "電話番号での検索" },
email = new { type = "string", description = "メールアドレスでの検索" },
acceptanceDate = new { type = "string", description = "受付日での検索(YYYY-MM-DD形式)" },
reservationDate = new { type = "string", description = "予約日での検索(YYYY-MM-DD形式)" },
timeZone = new { type = "string", description = "時間帯での検索" },
model = new { type = "string", description = "車種名での検索" },
plateNumber = new { type = "string", description = "登録番号での検索" },
color = new { type = "string", description = "塗色名での検索" },
remarks = new { type = "string", description = "備考での検索" },
page = new { type = "integer", description = "ページ番号(デフォルト: 1)" },
limit = new { type = "integer", description = "1ページあたりの件数(デフォルト: 10)" }
}
}
},
new ToolInfo
{
Name = "get_calendar",
Description = "指定した日付の予約カレンダー情報と空き状況を取得します。",
InputSchema = new
{
type = "object",
properties = new
{
date = new { type = "string", description = "カレンダー表示する日付(YYYY-MM-DD形式)" },
areaGroupId = new { type = "integer", description = "エリアグループID(オプション)" },
storeId = new { type = "integer", description = "店舗ID(オプション)" }
},
required = new[] { "date" }
}
}
};
return new ToolsListResult { Tools = tools };
}
/// <summary>
/// 指定されたツールを実行
/// </summary>
public async Task<ToolCallResult> CallToolAsync(string toolName, Dictionary<string, object>? arguments)
{
try
{
_logger.LogInformation("Calling tool: {ToolName} with arguments: {Arguments}",
toolName, JsonSerializer.Serialize(arguments));
string result = toolName switch
{
"search_reservations" => await CallSearchReservationsAsync(arguments),
"search_reservations_by_name" => await CallSearchReservationsByNameAsync(arguments),
"get_calendar" => await CallGetCalendarAsync(arguments),
_ => throw new ArgumentException($"Unknown tool: {toolName}")
};
return new ToolCallResult
{
Content = new List<ToolContent>
{
new ToolContent { Type = "text", Text = result }
},
IsError = false
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling tool: {ToolName}", toolName);
return new ToolCallResult
{
Content = new List<ToolContent>
{
new ToolContent { Type = "text", Text = $"Error: {ex.Message}" }
},
IsError = true
};
}
}
/// <summary>
/// 予約検索ツール実行
/// </summary>
private async Task<string> CallSearchReservationsAsync(Dictionary<string, object>? arguments)
{
var request = new ReservationSearchRequest();
if (arguments != null)
{
if (arguments.TryGetValue("CustomerName", out var customerName))
request.CustomerName = customerName?.ToString();
if (arguments.TryGetValue("CustomerNameKana", out var customerNameKana))
request.CustomerNameKana = customerNameKana?.ToString();
if (arguments.TryGetValue("PhoneNumber", out var phoneNumber))
request.PhoneNumber = phoneNumber?.ToString();
if (arguments.TryGetValue("Email", out var email))
request.Email = email?.ToString();
if (arguments.TryGetValue("ReceptionDate", out var receptionDate))
request.ReceptionDate = receptionDate?.ToString();
if (arguments.TryGetValue("AreaGroupId", out var areaGroupId) && int.TryParse(areaGroupId?.ToString(), out var areaGroupIdInt))
request.AreaGroupId = areaGroupIdInt;
if (arguments.TryGetValue("StoreId", out var storeId) && int.TryParse(storeId?.ToString(), out var storeIdInt))
request.StoreId = storeIdInt;
if (arguments.TryGetValue("Status", out var status))
request.Status = status?.ToString();
if (arguments.TryGetValue("page", out var page) && int.TryParse(page?.ToString(), out var pageInt))
request.Page = pageInt;
if (arguments.TryGetValue("perPage", out var perPage) && int.TryParse(perPage?.ToString(), out var perPageInt))
request.PerPage = perPageInt;
// 予約日の設定
if (arguments.TryGetValue("ReservationDate", out var reservationDate))
{
var dateStr = reservationDate?.ToString();
if (!string.IsNullOrEmpty(dateStr))
{
request.ReservationDate = new ReservationDateRange
{
Start = dateStr,
End = dateStr
};
}
}
// 時間帯の設定
if (arguments.TryGetValue("TimeZone", out var timeZone))
{
var timeZoneStr = timeZone?.ToString();
if (!string.IsNullOrEmpty(timeZoneStr))
{
request.TimeZone = new[] { timeZoneStr };
}
}
}
return await _webApiClient.SearchReservationsAsync(request);
}
/// <summary>
/// 名前による予約検索ツール実行
/// </summary>
private async Task<string> CallSearchReservationsByNameAsync(Dictionary<string, object>? arguments)
{
var request = new ReservationSearchByNameRequest();
if (arguments != null)
{
if (arguments.TryGetValue("SellingCompany", out var sellingCompany))
request.SellingCompany = sellingCompany?.ToString();
if (arguments.TryGetValue("Store", out var store))
request.Store = store?.ToString();
if (arguments.TryGetValue("AreaGroup", out var areaGroup))
request.AreaGroup = areaGroup?.ToString();
if (arguments.TryGetValue("CustomerName", out var customerName))
request.CustomerName = customerName?.ToString();
if (arguments.TryGetValue("PhoneNumber", out var phoneNumber))
request.PhoneNumber = phoneNumber?.ToString();
if (arguments.TryGetValue("Email", out var email))
request.Email = email?.ToString();
if (arguments.TryGetValue("AcceptanceDate", out var acceptanceDate))
request.AcceptanceDate = acceptanceDate?.ToString();
if (arguments.TryGetValue("Model", out var model))
request.Model = model?.ToString();
if (arguments.TryGetValue("PlateNumber", out var plateNumber))
request.PlateNumber = plateNumber?.ToString();
if (arguments.TryGetValue("Color", out var color))
request.Color = color?.ToString();
if (arguments.TryGetValue("Remarks", out var remarks))
request.Remarks = remarks?.ToString();
if (arguments.TryGetValue("page", out var page) && int.TryParse(page?.ToString(), out var pageInt))
request.Page = pageInt;
if (arguments.TryGetValue("limit", out var limit) && int.TryParse(limit?.ToString(), out var limitInt))
request.Limit = limitInt;
// 予約日の設定
if (arguments.TryGetValue("ReservationDate", out var reservationDate))
{
var dateStr = reservationDate?.ToString();
var storeValue = arguments.TryGetValue("Store", out var storeArg) ? storeArg?.ToString() : null;
if (!string.IsNullOrEmpty(dateStr))
{
request.ReservationDate = new ReservationDateRange
{
Store = storeValue,
Start = dateStr,
End = dateStr
};
}
}
// 時間帯の設定
if (arguments.TryGetValue("TimeZone", out var timeZone))
{
var timeZoneStr = timeZone?.ToString();
if (!string.IsNullOrEmpty(timeZoneStr))
{
request.TimeZone = new[] { timeZoneStr };
}
}
}
return await _webApiClient.SearchReservationsByNameAsync(request);
}
/// <summary>
/// カレンダー取得ツール実行
/// </summary>
private async Task<string> CallGetCalendarAsync(Dictionary<string, object>? arguments)
{
var request = new CalendarRequest();
if (arguments != null)
{
if (arguments.TryGetValue("date", out var date))
request.Date = date?.ToString() ?? string.Empty;
if (arguments.TryGetValue("AreaGroupId", out var areaGroupId) && int.TryParse(areaGroupId?.ToString(), out var areaGroupIdInt))
request.AreaGroupId = areaGroupIdInt;
}
if (string.IsNullOrEmpty(request.Date))
{
throw new ArgumentException("date parameter is required");
}
return await _webApiClient.GetCalendarAsync(request);
}
}
WebApiClient.cs
using System.Text.Json;
using KkcMcpServerApi.Models;
namespace KkcMcpServerApi.Services;
/// <summary>
/// WebAPIクライアントのインターフェース
/// </summary>
public interface IWebApiClient
{
Task<string> SearchReservationsAsync(ReservationSearchRequest request);
Task<string> SearchReservationsByNameAsync(ReservationSearchByNameRequest request);
Task<string> GetCalendarAsync(CalendarRequest request);
}
/// <summary>
/// kkc-webapi を呼び出すクライアント
/// </summary>
public class WebApiClient : IWebApiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<WebApiClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public WebApiClient(HttpClient httpClient, ILogger<WebApiClient> logger)
{
_httpClient = httpClient;
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
// Laravel APIはPascalCaseを期待しているため、PropertyNamingPolicyを設定しない
WriteIndented = true
};
}
/// <summary>
/// 予約検索API呼び出し
/// </summary>
public async Task<string> SearchReservationsAsync(ReservationSearchRequest request)
{
try
{
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
_logger.LogInformation("Calling reservations/search API with: {Request}", json);
var response = await _httpClient.PostAsync("/api/v1/reservations/search", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("API call failed with status {StatusCode}: {Error}",
response.StatusCode, errorContent);
throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
}
var result = await response.Content.ReadAsStringAsync();
_logger.LogInformation("API response received: {Response}", result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling reservations search API");
throw;
}
}
/// <summary>
/// 名前による予約検索API呼び出し
/// </summary>
public async Task<string> SearchReservationsByNameAsync(ReservationSearchByNameRequest request)
{
try
{
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
_logger.LogInformation("Calling reservations/search-by-name API with: {Request}", json);
var response = await _httpClient.PostAsync("/api/v1/reservations/search-by-name", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("API call failed with status {StatusCode}: {Error}",
response.StatusCode, errorContent);
throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
}
var result = await response.Content.ReadAsStringAsync();
_logger.LogInformation("API response received: {Response}", result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling reservations search-by-name API");
throw;
}
}
/// <summary>
/// カレンダーAPI呼び出し
/// </summary>
public async Task<string> GetCalendarAsync(CalendarRequest request)
{
try
{
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
_logger.LogInformation("Calling reservations/calendar API with: {Request}", json);
var response = await _httpClient.PostAsync("/api/v1/reservations/calendar", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("API call failed with status {StatusCode}: {Error}",
response.StatusCode, errorContent);
throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
}
var result = await response.Content.ReadAsStringAsync();
_logger.LogInformation("API response received: {Response}", result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling calendar API");
throw;
}
}
}
まあ、それでも AI エージェントを使ってバイブコード(Vibe Coding)する分には十分な品質を保っています。About ページならばセキュリティがどうという話もないし特殊なロジックを使っているわけではありません。単にアプリの製品情報とかを並べたいだけです。
従来の方式で言えば Android Studio や Visual Studio が出してくれるテンプレートを使えばいいのですが、 そこすら面倒くさいし、こうやって AI エージェントが作りやすいように画面設計や変数設定をしてしまったほうが後々楽でしょう。うまくいかなければ、もう一度いちから AI エージェントに作って貰えばよいです。
エミュレータで実行するとこんな感じです。
既存の Activity を編集してもらう
About 画面ができたので、いくつか編集をしてみましょう。
UI を作成している AboutScreen の中身を手作業で変更してもよいのですが、AI エージェントを使っても追加や削除ができます。手作業だとレイアウトが崩れたりするので、情報を与えて AI エージェントに修正して貰ったほうが作業が早く済みます。複雑なロジックの場合は面倒なのですが、ちょっとした文言を追加するのはプロンプトだけで結構いけます。