Implement TypeScript MCP server for Oracle Database
U You 2026-04-17 01:07

以下のTypeScriptのMCPサーバーの実装を、アタッチしているOracle Database上で動作するMCPサーバーのフレームワークに合わせて、Zodのスキーマ定義を表定義に、データの投入をINSERT文に、ファンクションはPL/SQLno

C Claude 2026-04-17 01:07
💭 Thinking
C Claude 2026-04-17 01:07

メッセージが途中で切れているようです。確認させてください:

  1. 1. 「アタッチしているTypeScriptのMCPサーバーの実装」とありますが、添付ファイルが見当たりません。対象のファイルパスを教えていただけますか?
  2. 2. 最後の文章「ファンクションはPL/SQLno」で途切れています。「PL/SQLのファンクションに変換する」というご要望でしょうか?
  3. 3. 変換先の「Oracle Database上で動作するMCPサーバーのフレームワーク」について、参照すべきリポジトリ内のサンプルやドキュメントはありますか?
U You 2026-04-17 01:09

@/Users/********/Documents/mcp-app @/Users/********/Documents/ext-apps/examples/cohort-heatmap-server/server.ts 1. ディレクトリmcp-appをアタッチしました。

  1. 2. PL/SQLのファンクションに変換してください。
  2. 3. server.tsをアタッチしました。

C Claude 2026-04-17 01:09
💭 Thinking
C Claude 2026-04-17 01:09

mcp-appフレームワークの構造を理解するため探索します。

C Claude 2026-04-17 01:09
Agent
{
  "description": "Explore mcp-app Oracle MCP framework",
  "subagent_type": "Explore",
  "prompt": "Explore the directory `/Users/********/Documents/mcp-app` thoroughly to understand how this Oracle Database MCP server framework works.\n\nI need to convert a TypeScript MCP server (which uses Zod schemas and in-memory data generation) into this framework. Specifically I need to understand:\n\n1. **Directory structure** — what's in `app/`, `examples/`, `src/`, `ords/`? Where do MCP servers live?\n2. **How an existing example MCP server is structured** — look at `examples/` and find a complete server example. Pay special attention to any \"budget-allocator-server\" example that was recently added (per recent commits).\n3. **How tools are defined** — How are tool schemas declared? Is there an output_schema definition pattern (per recent commit \"add output_schema definition\")? How are inputs/outputs typed?\n4. **Database setup** — How are tables created? Look at `install-tables.sql`, `install-sampleserver.sql`, `install-all*.sql`. What's the convention?\n5. **How PL/SQL functions are written and registered** — How does a tool call dispatch into PL/SQL? What's the calling convention (parameters, return types like JSON/CLOB/object types)?\n6. **How data insertion is done** — INSERT statement patterns, sample data files.\n7. **README.md content** — high-level architecture description.\n\nReport:\n- Tree structure of relevant directories (don't dump everything — just the parts relevant to defining a new MCP server)\n- File paths of: a complete example server (preferably budget-allocator), the install scripts that would need to be modified or added, any PL/SQL package skeleton\n- Code snippets showing: (a) how a tool is registered with input/output schema, (b) how a PL/SQL function returns structured data to the MCP layer, (c) how a CREATE TABLE statement is written for sample data\n- Any naming conventions (table names, schema names, package names)\n\nBe thorough — I need enough detail to actually write a new server end-to-end. ~500 words is fine."
}
U You 2026-04-17 01:11
✅ Result
Excellent! Now I have a comprehensive understanding. Let me create a detailed report for the user.

## Comprehensive Exploration Report: Oracle Database MCP Server Framework

I've thoroughly explored the `/Users/********/Documents/mcp-app` directory. Here's what you need to understand to convert a TypeScript MCP server into this Oracle Database framework:

---

### **1. Directory Structure**

```
mcp-app/
├── src/                     # Core MCP framework (shared across all servers)
│   ├── packages/           # PL/SQL packages (OJ_MCP_*)
│   │   ├── oj_mcp_jsonrpc_utils.*
│   │   ├── oj_mcp_app_utils.*
│   │   ├── oj_mcp_app_methods.*
│   │   └── oj_mcp_app_server.*    # Main MCP request dispatcher
│   ├── procedures/         # Request handlers
│   │   ├── oj_mcp_post_handler.pls
│   │   ├── oj_mcp_delete_handler.pls
│   ├── tools/              # Sample tool implementations
│   │   ├── get_schema.pls
│   │   ├── run_sql.pls
│   │   ├── get_current_user.pls
│   │   └── register_tools.sql    # Tool registration (uses uc_ai_tools_api)
│   ├── tables/
│   └── resources/          # UI resource definitions
├── examples/               # Complete server examples
│   ├── budget-allocator-server/  # Full-featured example (preferred template)
│   │   ├── bas_create_tables.sql
│   │   ├── bas_get_budget_data_response.sql
│   │   ├── bas_prepare_budget_*.sql  (sample data)
│   │   ├── output_schema.json   # Output schema definition
│   │   └── bas_install.sql
│   └── basic-server/              # Simpler example
├── ords/                   # ORDS REST module definitions
│   └── ORDS_REST_APEXDEV_sampleserver.sql
├── install-tables.sql
├── install-packages.sql
└── install-sampleserver.sql  # Main installation template
```

**Key insight:** Each MCP server is self-contained but uses the shared framework in `src/`. You add your server's tables, tools, and data under its own namespace (e.g., `bas_` prefix for budget-allocator).

---

### **2. How to Define a Complete Server: Budget-Allocator Example**

The **budget-allocator-server** (added in commit `8c8a0f6`) is the canonical reference. It demonstrates:

- **9 data tables** modeling nested Zod schemas (categories, config, historical months, analytics benchmarks)
- **1 response function** (`bas_get_budget_data_response`) that builds complex JSON from tables
- **1 output schema** (`output_schema.json`) defining the MCP tool output
- **Data initialization** via SQL INSERTs and stored procedures

File paths:
- `/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_create_tables.sql` (schema)
- `/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_get_budget_data_response.sql` (response function)
- `/Users/********/Documents/mcp-app/examples/budget-allocator-server/output_schema.json` (schema definition)

---

### **3. Tool Definition & Registration Pattern**

Tools are registered via the UC_AI framework (United Codes UC_AI). The pattern:

**Input schema** (JSON Schema 2020-...
C Claude 2026-04-17 01:11

budget-allocator-serverの実際の実装を詳しく見て、変換のパターンを把握します。

C Claude 2026-04-17 01:11
Bash
{
  "command": "ls /Users/********/Documents/mcp-app/examples/budget-allocator-server/",
  "description": "List budget-allocator-server files"
}
U You 2026-04-17 01:11
✅ Result
bas_create_tables.sql
bas_drop_all.sql
bas_generate_history.sql
bas_get_budget_data_response.sql
bas_install.sql
bas_prepare_budget_analytics.sql
bas_prepare_budget_categories.sql
bas_prepare_budget_config.sql
output_schema.json
C Claude 2026-04-17 01:11
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_install.sql"
}
U You 2026-04-17 01:11
✅ Result
1	set serveroutput on
2	-- create tables
3	@@bas_create_tables.sql
4	-- procedure to generate sample data.
5	@@bas_generate_history.sql
6	-- function to generate the response for tools call.
7	@@bas_get_budget_data_response.sql
8	-- insert test data 
9	@@bas_prepare_budget_categories.sql
10	@@bas_prepare_budget_config.sql
11	--
12	begin
13	    bas_generate_history;
14	end;
15	/
16	--
17	@@bas_prepare_budget_analytics.sql
18	commit;
19	-- generate tools call response
20	begin
21	    dbms_output.put_line(bas_get_budget_data_response);
22	end;
23	/
24	exit;
25	
C Claude 2026-04-17 01:11
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_create_tables.sql"
}
U You 2026-04-17 01:11
✅ Result
1	-- ============================================================
2	-- Budget Application Table Definitions for Oracle Database
3	-- ============================================================
4	
5	-- ------------------------------------------------------------
6	-- 1. bas_budget_categories
7	--    BudgetCategorySchema + BudgetCategoryInternal
8	-- ------------------------------------------------------------
9	create table bas_budget_categories (
10	    id               varchar2(100)   not null,
11	    name             varchar2(200)   not null,
12	    color            varchar2(50)    not null,
13	    default_percent  number(10, 4)   not null,
14	    trend_per_month  number(10, 4),
15	    constraint bas_pk_budget_categories primary key (id)
16	);
17	
18	-- ------------------------------------------------------------
19	-- 2. bas_budget_config
20	--    BudgetConfigSchema (scalar fields)
21	-- ------------------------------------------------------------
22	create table bas_budget_config (
23	    id               number          generated always as identity,
24	    default_budget   number(15, 4)   not null,
25	    currency         varchar2(10)    not null,
26	    currency_symbol  varchar2(10)    not null,
27	    constraint bas_pk_budget_config primary key (id)
28	);
29	
30	-- ------------------------------------------------------------
31	-- 3. bas_budget_config_preset_budgets
32	--    BudgetConfigSchema.presetBudgets: z.array(z.number())
33	-- ------------------------------------------------------------
34	create table bas_budget_config_preset_budgets (
35	    id               number          generated always as identity,
36	    config_id        number          not null,
37	    sort_order       number          not null,
38	    preset_value     number(15, 4)   not null,
39	    constraint bas_pk_config_presets    primary key (id),
40	    constraint bas_fk_presets_config    foreign key (config_id)
41	        references bas_budget_config (id) on delete cascade
42	);
43	
44	-- ------------------------------------------------------------
45	-- 4. bas_historical_months
46	--    HistoricalMonthSchema (parent)
47	-- ------------------------------------------------------------
48	create table bas_historical_months (
49	    id               number          generated always as identity,
50	    month            varchar2(20)    not null,
51	    constraint bas_pk_historical_months primary key (id),
52	    constraint bas_uq_historical_months unique (month)
53	);
54	
55	-- ------------------------------------------------------------
56	-- 5. bas_historical_month_allocations
57	--    HistoricalMonthSchema.allocations: z.record(z.string(), z.number())
58	-- ------------------------------------------------------------
59	create table bas_historical_month_allocations (
60	    id               number          generated always as identity,
61	    month_id         number          not null,
62	    category_id      varchar2(100)   not null,
63	    allocation       number(15...
C Claude 2026-04-17 01:11
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_get_budget_data_response.sql"
}
U You 2026-04-17 01:11
✅ Result
1	create or replace function bas_get_budget_data_response
2	    return clob
3	as
4	    -- root
5	    l_root              json_object_t := json_object_t();
6	
7	    -- config
8	    l_config            json_object_t := json_object_t();
9	    l_categories        json_array_t  := json_array_t();
10	    l_category          json_object_t;
11	    l_presets           json_array_t  := json_array_t();
12	
13	    -- analytics
14	    l_analytics         json_object_t := json_object_t();
15	    l_history           json_array_t  := json_array_t();
16	    l_month_obj         json_object_t;
17	    l_allocations       json_object_t;
18	    l_benchmarks        json_array_t  := json_array_t();
19	    l_benchmark_obj     json_object_t;
20	    l_cat_benchmarks    json_object_t;
21	    l_percentiles       json_object_t;
22	    l_stages            json_array_t  := json_array_t();
23	
24	    -- work
25	    l_analytics_id      bas_budget_analytics.id%type;
26	    l_default_stage     bas_budget_analytics.default_stage%type;
27	begin
28	    -- ============================================================
29	    -- config.categories
30	    -- trendPerMonth は BudgetCategorySchema 外のため除外
31	    -- ============================================================
32	    for r in (
33	        select id, name, color, default_percent
34	        from   bas_budget_categories
35	        order  by id
36	    ) loop
37	        l_category := json_object_t();
38	        l_category.put('id',             r.id);
39	        l_category.put('name',           r.name);
40	        l_category.put('color',          r.color);
41	        l_category.put('defaultPercent', r.default_percent);
42	        l_categories.append(l_category);
43	    end loop;
44	
45	    -- ============================================================
46	    -- config.presetBudgets
47	    -- ============================================================
48	    for r in (
49	        select preset_value
50	        from   bas_budget_config_preset_budgets
51	        where  config_id = (select id from bas_budget_config fetch first 1 rows only)
52	        order  by sort_order
53	    ) loop
54	        l_presets.append(r.preset_value);
55	    end loop;
56	
57	    -- ============================================================
58	    -- config
59	    -- ============================================================
60	    for r in (
61	        select default_budget, currency, currency_symbol
62	        from   bas_budget_config
63	        fetch first 1 rows only
64	    ) loop
65	        l_config.put('categories',     l_categories);
66	        l_config.put('presetBudgets',  l_presets);
67	        l_config.put('defaultBudget',  r.default_budget);
68	        l_config.put('currency',       r.currency);
69	        l_config.put('currencySymbol', r.currency_symbol);
70	    end loop;
71	
72	    -- ============================================================
73	    -- analytics parent
74	    -- ============================================================
75...
C Claude 2026-04-17 01:11
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/output_schema.json"
}
U You 2026-04-17 01:11
✅ Result
1	{
2	  "$schema": "https://json-schema.org/draft/2020-12/schema",
3	  "$id": "bas_budget_data_response.schema.json",
4	  "title": "BudgetDataResponse",
5	  "type": "object",
6	  "required": ["config", "analytics"],
7	  "additionalProperties": false,
8	  "properties": {
9	
10	    "config": {
11	      "title": "BudgetConfig",
12	      "description": "Static configuration for the budget application.",
13	      "type": "object",
14	      "required": ["categories", "presetBudgets", "defaultBudget", "currency", "currencySymbol"],
15	      "additionalProperties": false,
16	      "properties": {
17	
18	        "categories": {
19	          "description": "List of budget categories available in the application.",
20	          "type": "array",
21	          "items": {
22	            "title": "BudgetCategory",
23	            "description": "A single budget category definition.",
24	            "type": "object",
25	            "required": ["id", "name", "color", "defaultPercent"],
26	            "additionalProperties": false,
27	            "properties": {
28	              "id": {
29	                "description": "Unique identifier for the category.",
30	                "type": "string"
31	              },
32	              "name": {
33	                "description": "Human-readable display name of the category.",
34	                "type": "string"
35	              },
36	              "color": {
37	                "description": "Display color as a 6-digit hex code (e.g. #3b82f6).",
38	                "type": "string",
39	                "pattern": "^#[0-9a-fA-F]{6}$"
40	              },
41	              "defaultPercent": {
42	                "description": "Default allocation percentage for this category (0–100).",
43	                "type": "number",
44	                "minimum": 0,
45	                "maximum": 100
46	              }
47	            }
48	          }
49	        },
50	
51	        "presetBudgets": {
52	          "description": "Preset total budget amounts available for quick selection.",
53	          "type": "array",
54	          "items": {
55	            "description": "A preset budget amount. Must be greater than zero.",
56	            "type": "number",
57	            "exclusiveMinimum": 0
58	          }
59	        },
60	
61	        "defaultBudget": {
62	          "description": "Default total budget amount applied on initial load. Must be greater than zero.",
63	          "type": "number",
64	          "exclusiveMinimum": 0
65	        },
66	        "currency": {
67	          "description": "ISO 4217 currency code (e.g. USD).",
68	          "type": "string",
69	          "minLength": 3,
70	          "maxLength": 3
71	        },
72	        "currencySymbol": {
73	          "description": "Currency symbol used for display (e.g. $).",
74	          "type": "string",
75	          "minLength": 1
76	        }
77	      }
78	    },
79	
80	    "analytics": {
81	      "title": "BudgetAnalytics",
82	      "description": "Historical and benchmark analyti...
C Claude 2026-04-17 01:12
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_prepare_budget_categories.sql"
}
U You 2026-04-17 01:12
✅ Result
1	set define off
2	
3	insert into bas_budget_categories (id, name, color, default_percent, trend_per_month)
4	values ('marketing',   'Marketing',   '#3b82f6', 25, 0.15);
5	
6	insert into bas_budget_categories (id, name, color, default_percent, trend_per_month)
7	values ('engineering', 'Engineering', '#10b981', 35, -0.1);
8	
9	insert into bas_budget_categories (id, name, color, default_percent, trend_per_month)
10	values ('operations',  'Operations',  '#f59e0b', 15, 0.05);
11	
12	insert into bas_budget_categories (id, name, color, default_percent, trend_per_month)
13	values ('sales',       'Sales',       '#ef4444', 15, 0.08);
14	
15	insert into bas_budget_categories (id, name, color, default_percent, trend_per_month)
16	values ('rd',          'R&D',         '#8b5cf6', 10, -0.18);
17	
18	commit;
19	
20	set define on
C Claude 2026-04-17 01:12
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/budget-allocator-server/bas_generate_history.sql"
}
U You 2026-04-17 01:12
✅ Result
1	-- ============================================================
2	-- bas_generate_history
3	-- Generate 24 months of historical allocation data
4	-- Port of TypeScript generateHistory() with seededRandom(42)
5	-- ============================================================
6	create or replace procedure bas_generate_history
7	as
8	    -- ---------------------------------------------------------
9	    -- Seeded LCG: seed = (seed * 1103515245 + 12345) & 0x7fffffff
10	    -- PL/SQL has no unsigned 32-bit, so use MOD 2^31 (= 2147483648)
11	    -- to reproduce the JS bitwise & 0x7fffffff behaviour.
12	    -- ---------------------------------------------------------
13	    v_seed          number := 42;
14	    v_modulus       constant number := 2147483648; -- 0x80000000
15	
16	    -- Category row
17	    type t_cat is record (
18	        id              bas_budget_categories.id%type,
19	        default_percent bas_budget_categories.default_percent%type,
20	        trend_per_month bas_budget_categories.trend_per_month%type
21	    );
22	    type t_cat_tab is table of t_cat index by pls_integer;
23	    v_cats          t_cat_tab;
24	
25	    -- Raw allocation accumulator per month
26	    type t_alloc is table of number index by varchar2(100);
27	    v_raw_alloc     t_alloc;
28	
29	    v_month_date    date;
30	    v_month_str     varchar2(7);   -- 'YYYY-MM'
31	    v_month_id      bas_historical_months.id%type;
32	    v_months_from_start number;
33	    v_trend         number;
34	    v_noise         number;
35	    v_raw           number;
36	    v_total         number;
37	    v_cat_id        varchar2(100);
38	    v_rnd           number;        -- value in [0, 1)
39	
40	    -- ---------------------------------------------------------
41	    -- Inner function: advance seed and return value in [0, 1)
42	    -- Matches JS: seed = (seed * 1103515245 + 12345) & 0x7fffffff
43	    --             return seed / 0x7fffffff
44	    -- ---------------------------------------------------------
45	    function seeded_random return number
46	    as
47	    begin
48	        v_seed := mod(v_seed * 1103515245 + 12345, v_modulus);
49	        return v_seed / 2147483647; -- 0x7fffffff
50	    end seeded_random;
51	
52	begin
53	    -- Load all categories once
54	    select id, default_percent, nvl(trend_per_month, 0)
55	    bulk collect into v_cats
56	    from bas_budget_categories
57	    order by id;
58	
59	    if v_cats.count = 0 then
60	        raise_application_error(-20001, 'bas_budget_categories is empty');
61	    end if;
62	
63	    -- Month loop: i = 23 downto 0  (24 months ending this month)
64	    for i in reverse 0..23 loop
65	
66	        v_months_from_start := 23 - i;
67	        v_month_date := add_months(trunc(sysdate, 'MM'), -i);
68	        v_month_str  := to_char(v_month_date, 'YYYY-MM');
69	
70	        -- Upsert into bas_historical_months
71	        begin
72	            insert into bas_historical_months (month)
73	            values (v_month_str)
74	            returning i...
C Claude 2026-04-17 01:12
💭 Thinking
C Claude 2026-04-17 01:12

Now I have a complete picture. Let me check tool registration for the output_schema handling.

C Claude 2026-04-17 01:12
Bash
{
  "command": "ls /Users/********/Documents/mcp-app/examples/basic-server/ 2>/dev/null && echo \"---\" && ls /Users/********/Documents/mcp-app/examples/",
  "description": "List example servers"
}
U You 2026-04-17 01:12
✅ Result
register-basic-servers.sql
---
basic-server
budget-allocator-server
C Claude 2026-04-17 01:12
Read
{
  "file_path": "/Users/********/Documents/mcp-app/examples/basic-server/register-basic-servers.sql"
}
U You 2026-04-17 01:12
✅ Result
1	declare
2	    /*  Tools */
3	    type t_apps is table of varchar2(40);
4	    l_apps t_apps := t_apps(
5	        'get-time-preact',
6	        'get-time-react',
7	        'get-time-solid',
8	        'get-time-svelte',
9	        'get-time-vue'
10	    );
11	    l_app varchar2(40);
12	    C_INPUT_SCHEMA constant json_object_t :=  json_object_t.parse('{"$schema":"http://json-schema.org/draft-07/schema#","type":"object"}');
13	    C_OUTPUT_SCHEMA constant clob := q'~
14	{
15	  "$schema": "http://json-schema.org/draft-07/schema#",
16	  "type": "object",
17	  "properties": {
18	    "time": {
19	      "type": "string"
20	    }
21	  }
22	}    
23	    ~';
24	    l_tool_id uc_ai_tools.id%type;
25	    /* UI resources */
26	    l_resource_id number;
27	    l_update_user    varchar2(80);
28	    l_update_time    timestamp;
29	    C_DEFAULT_HTML constant clob := q'~<!DOCTYPE html><html><body></body></html>~';
30	begin
31	    l_update_user := sys_context('USERENV', 'CURRENT_USER');
32	    l_update_time := current_timestamp;
33	    for i in 1..l_apps.count
34	    loop
35	        l_app := l_apps(i);
36	        /* delete and create tool */
37	        delete from uc_ai_tools where code = l_app;
38	        l_tool_id := uc_ai_tools_api.create_tool_from_schema(
39	            p_tool_code => l_app,
40	            p_description => 'Returns the current server time as an ISO 8601 string.',
41	            p_function_call => q'~return get_time();~',
42	            p_json_schema => C_INPUT_SCHEMA,
43	            p_tags => apex_t_varchar2('ext-apps')
44	        );
45	        /* create default UI resource, text should be updated later */
46	        merge into oj_mcp_ui_resources t
47	        using (
48	            select
49	                l_app                                name,
50	                'ui://' || l_app || '/mcp-app.html'  uri,
51	                'text/html;profile=mcp-app'          mime_type,
52	                'Returns the current server time as an ISO 8601 string.' description,
53	                C_DEFAULT_HTML                       text
54	            from dual
55	        ) s
56	        on (t.name = s.name)
57	        when matched then
58	            update set
59	                t.uri         = s.uri,
60	                t.mime_type   = s.mime_type,
61	                t.description = s.description,
62	                t.text        = case when t.text is null then s.text else t.text end,
63	                t.updated_by  = l_update_user,
64	                t.updated_at  = l_update_time
65	        when not matched then
66	            insert (name, uri, mime_type, description, text, created_by, created_at, updated_by, updated_at)
67	            values (s.name, s.uri, s.mime_type, s.description, s.text, l_update_user, l_update_time, l_update_user, l_update_time)
68	        ;
69	        /* register ui resource to the tool */
70	        select id into l_tool_id from uc_ai_tools where code = l_app;
71	        select id into l_resource_id from oj_mcp_ui_resources where n...
C Claude 2026-04-17 01:13
💭 Thinking
C Claude 2026-04-17 01:13

理解しました。budget-allocator-server のパターンに完全に倣って、cohort-heatmap-server をOracle向けに変換します。

ディレクトリを作成し、各ファイルを書き出します。

C Claude 2026-04-17 01:13
Bash
{
  "command": "mkdir -p /Users/********/Documents/mcp-app/examples/cohort-heatmap-server && ls /Users/********/Documents/mcp-app/examples/",
  "description": "Create cohort-heatmap-server directory"
}
U You 2026-04-17 01:13
✅ Result
basic-server
budget-allocator-server
cohort-heatmap-server
C Claude 2026-04-17 01:14
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_create_tables.sql",
  "content": "-- ============================================================\n-- Cohort Heatmap Application Table Definitions for Oracle Database\n-- Port of cohort-heatmap-server/server.ts (Zod schemas)\n-- ============================================================\n\n-- ------------------------------------------------------------\n-- 1. chs_metric_params\n--    Internal RetentionParams keyed by metric name\n--    (paramsMap in TypeScript: retention / revenue / active)\n-- ------------------------------------------------------------\ncreate table chs_metric_params (\n    metric           varchar2(20)    not null,\n    base_retention   number(10, 4)   not null,\n    decay_rate       number(10, 4)   not null,\n    floor_value      number(10, 4)   not null,\n    noise            number(10, 4)   not null,\n    constraint chs_pk_metric_params primary key (metric),\n    constraint chs_ck_metric_name\n        check (metric in ('retention', 'revenue', 'active'))\n);\n\n-- ------------------------------------------------------------\n-- 2. chs_dataset_meta\n--    CohortDataSchema scalar fields: metric, periodType, generatedAt\n-- ------------------------------------------------------------\ncreate table chs_dataset_meta (\n    id               number          generated always as identity,\n    metric           varchar2(20)    not null,\n    period_type      varchar2(20)    not null,\n    generated_at     timestamp       default systimestamp not null,\n    constraint chs_pk_dataset_meta primary key (id),\n    constraint chs_fk_meta_metric foreign key (metric)\n        references chs_metric_params (metric),\n    constraint chs_ck_period_type\n        check (period_type in ('monthly', 'weekly'))\n);\n\n-- ------------------------------------------------------------\n-- 3. chs_periods\n--    CohortDataSchema.periods[] / CohortDataSchema.periodLabels[]\n-- ------------------------------------------------------------\ncreate table chs_periods (\n    period_index     number          not null,\n    period_code      varchar2(10)    not null,\n    period_label     varchar2(50)    not null,\n    constraint chs_pk_periods primary key (period_index)\n);\n\n-- ------------------------------------------------------------\n-- 4. chs_cohorts\n--    CohortRowSchema (parent): cohortId, cohortLabel, originalUsers\n-- ------------------------------------------------------------\ncreate table chs_cohorts (\n    cohort_index     number          not null,\n    cohort_id        varchar2(20)    not null,\n    cohort_label     varchar2(50)    not null,\n    original_users   number          not null,\n    constraint chs_pk_cohorts primary key (cohort_index)\n);\n\n-- ------------------------------------------------------------\n-- 5. chs_cohort_cells\n--    CohortRowSchema.cells[] = CohortCellSchema\n-- ------------------------------------------------------------\ncreate table chs_cohort_cells (\n    cohort_index     number          not null,\n    period_index     number          not null,\n    retention        number(10, 6)   not null,\n    users_retained   number          not null,\n    users_original   number          not null,\n    constraint chs_pk_cohort_cells primary key (cohort_index, period_index),\n    constraint chs_fk_cells_cohort foreign key (cohort_index)\n        references chs_cohorts (cohort_index) on delete cascade\n);\n\n-- ============================================================\n-- Comments\n-- ============================================================\ncomment on table chs_metric_params   is 'RetentionParams per metric (paramsMap)';\ncomment on table chs_dataset_meta    is 'CohortDataSchema scalar fields';\ncomment on table chs_periods         is 'CohortDataSchema.periods[] / periodLabels[]';\ncomment on table chs_cohorts         is 'CohortRowSchema parent';\ncomment on table chs_cohort_cells    is 'CohortRowSchema.cells[] = CohortCellSchema';\n"
}
U You 2026-04-17 01:14
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_create_tables.sql
C Claude 2026-04-17 01:14
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_prepare_metric_params.sql",
  "content": "set define off\n\n-- Mirror of TypeScript paramsMap in generateCohortData()\ninsert into chs_metric_params (metric, base_retention, decay_rate, floor_value, noise)\nvalues ('retention', 0.75, 0.12, 0.08, 0.04);\n\ninsert into chs_metric_params (metric, base_retention, decay_rate, floor_value, noise)\nvalues ('revenue',   0.70, 0.10, 0.15, 0.06);\n\ninsert into chs_metric_params (metric, base_retention, decay_rate, floor_value, noise)\nvalues ('active',    0.60, 0.18, 0.05, 0.05);\n\ncommit;\n\nset define on\n"
}
U You 2026-04-17 01:14
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_prepare_metric_params.sql
C Claude 2026-04-17 01:14
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_generate_cohort_data.sql",
  "content": "-- ============================================================\n-- chs_generate_cohort_data\n-- Port of TypeScript generateCohortData() / generateRetention()\n-- Uses seeded LCG (seed = 42) for deterministic output, matching\n-- the bas_generate_history convention.\n-- ============================================================\ncreate or replace procedure chs_generate_cohort_data(\n    p_metric        in varchar2 default 'retention',\n    p_period_type   in varchar2 default 'monthly',\n    p_cohort_count  in number   default 12,\n    p_max_periods   in number   default 12\n)\nas\n    -- ---------------------------------------------------------\n    -- Seeded LCG (matches bas_generate_history)\n    -- seed = (seed * 1103515245 + 12345) mod 2^31\n    -- ---------------------------------------------------------\n    v_seed       number := 42;\n    v_modulus    constant number := 2147483648; -- 0x80000000\n\n    -- RetentionParams loaded from chs_metric_params\n    v_base_ret   chs_metric_params.base_retention%type;\n    v_decay      chs_metric_params.decay_rate%type;\n    v_floor      chs_metric_params.floor_value%type;\n    v_noise_p    chs_metric_params.noise%type;\n\n    -- Loop locals\n    v_cohort_date    date;\n    v_cohort_id      varchar2(20);\n    v_cohort_label   varchar2(50);\n    v_original       number;\n    v_periods_avail  number;\n    v_p_max          number;\n    v_retention      number;\n    v_prev_ret       number;\n    v_base           number;\n    v_variation      number;\n    v_rnd            number;\n\n    -- ---------------------------------------------------------\n    -- Inner: advance seed and return value in [0, 1)\n    -- ---------------------------------------------------------\n    function seeded_random return number\n    as\n    begin\n        v_seed := mod(v_seed * 1103515245 + 12345, v_modulus);\n        return v_seed / 2147483647; -- 0x7fffffff\n    end seeded_random;\n\nbegin\n    -- Load metric params (raises NO_DATA_FOUND if metric is unknown)\n    select base_retention, decay_rate, floor_value, noise\n    into   v_base_ret, v_decay, v_floor, v_noise_p\n    from   chs_metric_params\n    where  metric = p_metric;\n\n    -- Idempotent reset (cells deleted via FK cascade)\n    delete from chs_cohorts;\n    delete from chs_periods;\n    delete from chs_dataset_meta;\n\n    -- Insert dataset meta\n    insert into chs_dataset_meta (metric, period_type)\n    values (p_metric, p_period_type);\n\n    -- ============================================================\n    -- Period headers: M0..M(maxPeriods-1) / \"Month 0\"..\"Month N\"\n    -- ============================================================\n    for p in 0 .. p_max_periods - 1 loop\n        insert into chs_periods (period_index, period_code, period_label)\n        values (\n            p,\n            'M' || p,\n            case when p = 0 then 'Month 0' else 'Month ' || p end\n        );\n    end loop;\n\n    -- ============================================================\n    -- Cohorts (oldest first)\n    -- ============================================================\n    for c in 0 .. p_cohort_count - 1 loop\n\n        v_cohort_date  := add_months(trunc(sysdate, 'MM'), -(p_cohort_count - 1 - c));\n        v_cohort_id    := to_char(v_cohort_date, 'YYYY-MM');\n        v_cohort_label := to_char(\n                              v_cohort_date,\n                              'Mon YYYY',\n                              'NLS_DATE_LANGUAGE=AMERICAN'\n                          );\n\n        -- originalUsers = floor(1000 + random * 4000)\n        v_rnd      := seeded_random();\n        v_original := floor(1000 + v_rnd * 4000);\n\n        insert into chs_cohorts (cohort_index, cohort_id, cohort_label, original_users)\n        values (c, v_cohort_id, v_cohort_label, v_original);\n\n        -- Newer cohorts have fewer periods\n        v_periods_avail := p_cohort_count - c;\n        v_p_max         := least(v_periods_avail, p_max_periods);\n        v_prev_ret      := 1.0;\n\n        for p in 0 .. v_p_max - 1 loop\n            -- generateRetention(): period 0 always returns 1.0\n            if p = 0 then\n                v_retention := 1.0;\n            else\n                v_rnd       := seeded_random();\n                v_base      := v_base_ret * exp(-v_decay * (p - 1)) + v_floor;\n                v_variation := (v_rnd - 0.5) * 2 * v_noise_p;\n                v_retention := greatest(0, least(1, v_base + v_variation));\n            end if;\n\n            -- Retention must not jump above previous + 0.02\n            v_retention := least(v_retention, v_prev_ret + 0.02);\n            v_prev_ret  := v_retention;\n\n            insert into chs_cohort_cells\n                (cohort_index, period_index, retention, users_retained, users_original)\n            values (\n                c,\n                p,\n                v_retention,\n                round(v_original * v_retention),\n                v_original\n            );\n        end loop;\n\n    end loop;\n\n    commit;\nend chs_generate_cohort_data;\n/\n"
}
U You 2026-04-17 01:14
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_generate_cohort_data.sql
C Claude 2026-04-17 01:15
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql",
  "content": "create or replace function chs_get_cohort_data_response\n    return clob\nas\n    -- root\n    l_root           json_object_t := json_object_t();\n\n    -- cohorts[]\n    l_cohorts        json_array_t  := json_array_t();\n    l_cohort_obj     json_object_t;\n    l_cells          json_array_t;\n    l_cell           json_object_t;\n\n    -- periods[] / periodLabels[]\n    l_periods        json_array_t  := json_array_t();\n    l_period_labels  json_array_t  := json_array_t();\n\n    -- meta\n    l_metric         chs_dataset_meta.metric%type;\n    l_period_type    chs_dataset_meta.period_type%type;\n    l_generated_at   chs_dataset_meta.generated_at%type;\nbegin\n    -- ============================================================\n    -- meta: metric / periodType / generatedAt\n    -- ============================================================\n    select metric, period_type, generated_at\n    into   l_metric, l_period_type, l_generated_at\n    from   chs_dataset_meta\n    fetch first 1 rows only;\n\n    -- ============================================================\n    -- periods[] and periodLabels[]\n    -- ============================================================\n    for r in (\n        select period_code, period_label\n        from   chs_periods\n        order  by period_index\n    ) loop\n        l_periods.append(r.period_code);\n        l_period_labels.append(r.period_label);\n    end loop;\n\n    -- ============================================================\n    -- cohorts[] (with nested cells[])\n    -- ============================================================\n    for ch in (\n        select cohort_index, cohort_id, cohort_label, original_users\n        from   chs_cohorts\n        order  by cohort_index\n    ) loop\n        l_cohort_obj := json_object_t();\n        l_cells      := json_array_t();\n\n        for cell in (\n            select cohort_index, period_index, retention,\n                   users_retained, users_original\n            from   chs_cohort_cells\n            where  cohort_index = ch.cohort_index\n            order  by period_index\n        ) loop\n            l_cell := json_object_t();\n            l_cell.put('cohortIndex',   cell.cohort_index);\n            l_cell.put('periodIndex',   cell.period_index);\n            l_cell.put('retention',     cell.retention);\n            l_cell.put('usersRetained', cell.users_retained);\n            l_cell.put('usersOriginal', cell.users_original);\n            l_cells.append(l_cell);\n        end loop;\n\n        l_cohort_obj.put('cohortId',      ch.cohort_id);\n        l_cohort_obj.put('cohortLabel',   ch.cohort_label);\n        l_cohort_obj.put('originalUsers', ch.original_users);\n        l_cohort_obj.put('cells',         l_cells);\n        l_cohorts.append(l_cohort_obj);\n    end loop;\n\n    -- ============================================================\n    -- CohortDataSchema (root)\n    -- ============================================================\n    l_root.put('cohorts',      l_cohorts);\n    l_root.put('periods',      l_periods);\n    l_root.put('periodLabels', l_period_labels);\n    l_root.put('metric',       l_metric);\n    l_root.put('periodType',   l_period_type);\n    l_root.put(\n        'generatedAt',\n        to_char(\n            l_generated_at at time zone 'UTC',\n            'YYYY-MM-DD\"T\"HH24:MI:SS.FF3\"Z\"'\n        )\n    );\n\n    return l_root.to_clob();\nend chs_get_cohort_data_response;\n/\n"
}
U You 2026-04-17 01:15
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql
C Claude 2026-04-17 01:15
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/output_schema.json",
  "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"chs_cohort_data_response.schema.json\",\n  \"title\": \"CohortData\",\n  \"description\": \"Cohort retention heatmap dataset.\",\n  \"type\": \"object\",\n  \"required\": [\"cohorts\", \"periods\", \"periodLabels\", \"metric\", \"periodType\", \"generatedAt\"],\n  \"additionalProperties\": false,\n  \"properties\": {\n\n    \"cohorts\": {\n      \"description\": \"Per-cohort retention rows ordered oldest first.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"title\": \"CohortRow\",\n        \"description\": \"A single cohort row with its retention cells.\",\n        \"type\": \"object\",\n        \"required\": [\"cohortId\", \"cohortLabel\", \"originalUsers\", \"cells\"],\n        \"additionalProperties\": false,\n        \"properties\": {\n          \"cohortId\": {\n            \"description\": \"Cohort identifier in YYYY-MM format.\",\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]{4}-(0[1-9]|1[0-2])$\"\n          },\n          \"cohortLabel\": {\n            \"description\": \"Human-readable cohort label (e.g. 'Jan 2026').\",\n            \"type\": \"string\"\n          },\n          \"originalUsers\": {\n            \"description\": \"Number of users in the cohort at period 0.\",\n            \"type\": \"number\",\n            \"minimum\": 0\n          },\n          \"cells\": {\n            \"description\": \"Retention cells for this cohort, ordered by periodIndex ascending.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"CohortCell\",\n              \"description\": \"Retention measurement for one cohort/period intersection.\",\n              \"type\": \"object\",\n              \"required\": [\n                \"cohortIndex\",\n                \"periodIndex\",\n                \"retention\",\n                \"usersRetained\",\n                \"usersOriginal\"\n              ],\n              \"additionalProperties\": false,\n              \"properties\": {\n                \"cohortIndex\": {\n                  \"description\": \"Zero-based row index of the cohort.\",\n                  \"type\": \"number\",\n                  \"minimum\": 0\n                },\n                \"periodIndex\": {\n                  \"description\": \"Zero-based column index of the period (0 = signup period).\",\n                  \"type\": \"number\",\n                  \"minimum\": 0\n                },\n                \"retention\": {\n                  \"description\": \"Retention rate in [0, 1].\",\n                  \"type\": \"number\",\n                  \"minimum\": 0,\n                  \"maximum\": 1\n                },\n                \"usersRetained\": {\n                  \"description\": \"Number of users still active in this period.\",\n                  \"type\": \"number\",\n                  \"minimum\": 0\n                },\n                \"usersOriginal\": {\n                  \"description\": \"Original cohort size (constant across cells of the same cohort).\",\n                  \"type\": \"number\",\n                  \"minimum\": 0\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n\n    \"periods\": {\n      \"description\": \"Period column codes (e.g. 'M0', 'M1', ...).\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n\n    \"periodLabels\": {\n      \"description\": \"Human-readable period labels (e.g. 'Month 0', 'Month 1', ...).\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n\n    \"metric\": {\n      \"description\": \"Retention metric this dataset was generated for.\",\n      \"type\": \"string\",\n      \"enum\": [\"retention\", \"revenue\", \"active\"]\n    },\n\n    \"periodType\": {\n      \"description\": \"Period granularity used to build the cohorts.\",\n      \"type\": \"string\",\n      \"enum\": [\"monthly\", \"weekly\"]\n    },\n\n    \"generatedAt\": {\n      \"description\": \"ISO 8601 UTC timestamp when the dataset was generated.\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    }\n  }\n}\n"
}
U You 2026-04-17 01:15
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/output_schema.json
C Claude 2026-04-17 01:15
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_install.sql",
  "content": "set serveroutput on\n-- create tables\n@@chs_create_tables.sql\n-- procedure to generate sample data.\n@@chs_generate_cohort_data.sql\n-- function to generate the response for tools call.\n@@chs_get_cohort_data_response.sql\n-- insert configuration data (RetentionParams per metric)\n@@chs_prepare_metric_params.sql\n--\nbegin\n    chs_generate_cohort_data;\nend;\n/\ncommit;\n-- generate tools call response\nbegin\n    dbms_output.put_line(chs_get_cohort_data_response);\nend;\n/\nexit;\n"
}
U You 2026-04-17 01:15
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_install.sql
C Claude 2026-04-17 01:15
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_drop_all.sql",
  "content": "-- ============================================================\n-- Drop all cohort-heatmap-server objects\n-- ============================================================\ndrop function chs_get_cohort_data_response;\ndrop procedure chs_generate_cohort_data;\n\ndrop table chs_cohort_cells   cascade constraints purge;\ndrop table chs_cohorts        cascade constraints purge;\ndrop table chs_periods        cascade constraints purge;\ndrop table chs_dataset_meta   cascade constraints purge;\ndrop table chs_metric_params  cascade constraints purge;\nexit;\n"
}
U You 2026-04-17 01:15
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_drop_all.sql
C Claude 2026-04-17 01:15
💭 Thinking
C Claude 2026-04-17 01:15
Bash
{
  "command": "ls -la /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/",
  "description": "Verify created files"
}
U You 2026-04-17 01:15
✅ Result
total 64
drwxr-xr-x@ 9 ********  staff   288 Apr 17 10:15 .
drwxr-xr-x  5 ********  staff   160 Apr 17 10:13 ..
-rw-r--r--@ 1 ********  staff  3815 Apr 17 10:14 chs_create_tables.sql
-rw-r--r--@ 1 ********  staff   547 Apr 17 10:15 chs_drop_all.sql
-rw-r--r--@ 1 ********  staff  4943 Apr 17 10:14 chs_generate_cohort_data.sql
-rw-r--r--@ 1 ********  staff  3365 Apr 17 10:15 chs_get_cohort_data_response.sql
-rw-r--r--@ 1 ********  staff   469 Apr 17 10:15 chs_install.sql
-rw-r--r--@ 1 ********  staff   499 Apr 17 10:14 chs_prepare_metric_params.sql
-rw-r--r--@ 1 ********  staff  3813 Apr 17 10:15 output_schema.json
C Claude 2026-04-17 01:16
budget-allocator-server のパターンに合わせて、cohort-heatmap-server のOracle版を作成しました(プレフィックスは chs_)。

変換マッピング

| TypeScript (server.ts) | Oracle (chs_*) |
|---|---|
| paramsMap (RetentionParams) | テーブル [chs_metric_params](examples/cohort-heatmap-server/chs_create_tables.sql) |
| CohortDataSchema のスカラ (metric/periodType/generatedAt) | テーブル chs_dataset_meta |
| periods[] / periodLabels[] | テーブル chs_periods |
| CohortRowSchema (親) | テーブル chs_cohorts |
| CohortRowSchema.cells[] (CohortCellSchema) | テーブル chs_cohort_cells (FK cascade) |
| generateCohortData() / generateRetention() | プロシージャ [chs_generate_cohort_data](examples/cohort-heatmap-server/chs_generate_cohort_data.sql) (シード付きLCG) |
| ツールハンドラの戻り (structuredContent) | ファンクション [chs_get_cohort_data_response](examples/cohort-heatmap-server/chs_get_cohort_data_response.sql) → CLOB |
| CohortDataSchema (Zod) | [output_schema.json](examples/cohort-heatmap-server/output_schema.json) (JSON Schema 2020-12) |

作成ファイル

  • [chs_create_tables.sql](examples/cohort-heatmap-server/chs_create_tables.sql) — 5テーブル
  • [chs_prepare_metric_params.sql](examples/cohort-heatmap-server/chs_prepare_metric_params.sql) — 3メトリクスのINSERT
  • [chs_generate_cohort_data.sql](examples/cohort-heatmap-server/chs_generate_cohort_data.sql) — シード付きLCG(seed=42)でJSの <code class="inline-code">Math.random</code> を再現
  • [chs_get_cohort_data_response.sql](examples/cohort-heatmap-server/chs_get_cohort_data_response.sql) — <code class="inline-code">JSON_OBJECT_T</code>/<code class="inline-code">JSON_ARRAY_T</code> でCLOB組み立て
  • [output_schema.json](examples/cohort-heatmap-server/output_schema.json)
  • [chs_install.sql](examples/cohort-heatmap-server/chs_install.sql) — <code class="inline-code">bas_install.sql</code> と同形式
  • [chs_drop_all.sql](examples/cohort-heatmap-server/chs_drop_all.sql)

注意点

  1. 1. <code class="inline-code">bas_*</code> と同様、ツール関数は引数を受け取らない設計のため、入力スキーマ (<code class="inline-code">metric</code>/<code class="inline-code">periodType</code>/<code class="inline-code">cohortCount</code>/<code class="inline-code">maxPeriods</code>) はプロシージャ <code class="inline-code">chs_generate_cohort_data</code> のデフォルト値で表現しています。インストール時は <code class="inline-code">retention/monthly/12/12</code> で生成。
  2. 2. ツール登録 (<code class="inline-code">uc_ai_tools_api.create_tool_from_schema</code> + <code class="inline-code">oj_mcp_tools_extras</code> に <code class="inline-code">output_schema</code> をセット) は <code class="inline-code">examples/basic-server/register-basic-servers.sql</code> のパターンに倣って別途追加が必要です。必要であれば続けて作成します。
U You 2026-04-17 01:22

server.tsの以下の定義をJSON Schemaの定義に変換して。
// Schemas - types are derived from these using z.infer
const GetCohortDataInputSchema = z.object({
metric: z
.enum(["retention", "revenue", "active"])
.optional()
.default("retention"),
periodType: z.enum(["monthly", "weekly"]).optional().default("monthly"),
cohortCount: z.number().min(3).max(24).optional().default(12),
maxPeriods: z.number().min(3).max(24).optional().default(12),
});

C Claude 2026-04-17 01:22
💭 Thinking
C Claude 2026-04-17 01:22
GetCohortDataInputSchema をJSON Schema (draft-07、basic-server の入力スキーマ形式に準拠) に変換しました。output_schema.json と並べて保存します。
C Claude 2026-04-17 01:22
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/input_schema.json",
  "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"chs_get_cohort_data_input.schema.json\",\n  \"title\": \"GetCohortDataInput\",\n  \"description\": \"Input parameters for the get-cohort-data tool.\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"metric\": {\n      \"description\": \"Retention metric to compute.\",\n      \"type\": \"string\",\n      \"enum\": [\"retention\", \"revenue\", \"active\"],\n      \"default\": \"retention\"\n    },\n    \"periodType\": {\n      \"description\": \"Period granularity used to build the cohorts.\",\n      \"type\": \"string\",\n      \"enum\": [\"monthly\", \"weekly\"],\n      \"default\": \"monthly\"\n    },\n    \"cohortCount\": {\n      \"description\": \"Number of cohorts to generate.\",\n      \"type\": \"number\",\n      \"minimum\": 3,\n      \"maximum\": 24,\n      \"default\": 12\n    },\n    \"maxPeriods\": {\n      \"description\": \"Maximum number of periods (columns) per cohort.\",\n      \"type\": \"number\",\n      \"minimum\": 3,\n      \"maximum\": 24,\n      \"default\": 12\n    }\n  }\n}\n"
}
U You 2026-04-17 01:22
✅ Result
File created successfully at: /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/input_schema.json
C Claude 2026-04-17 01:22

保存しました: [input_schema.json](examples/cohort-heatmap-server/input_schema.json)

変換マッピング

| Zod | JSON Schema |
|---|---|
| z.enum([...]) | "type": "string" + "enum": [...] |
| .optional().default(x) | required から外し "default": x を付与 |
| z.number().min(a).max(b) | "type": "number" + "minimum": a + "maximum": b |
| z.object({...}) (strict) | "type": "object" + "additionalProperties": false |

全フィールドが .optional().default(...) のため required 配列はありません。

U You 2026-04-17 01:24

PL/SQLファンクションchs_get_cohort_data_responseは、CLOB型の引数p_parametersを持ち、input_schema.jsonで定義されるJSONドキュメントを受け付けるように改変して。

C Claude 2026-04-17 01:25
💭 Thinking
C Claude 2026-04-17 01:25
chs_get_cohort_data_responsep_parameters CLOB を受け取るように改変します。入力JSONをパースし、デフォルト値を適用し、検証してから chs_generate_cohort_data を呼んで再生成、その後JSONを返します。
C Claude 2026-04-17 01:25
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql",
  "content": "create or replace function chs_get_cohort_data_response(\n    p_parameters in clob default null\n) return clob\nas\n    -- ---------------------------------------------------------\n    -- Parsed inputs (defaults mirror input_schema.json)\n    -- ---------------------------------------------------------\n    l_params         json_object_t;\n    l_metric         varchar2(20)  := 'retention';\n    l_period_type    varchar2(20)  := 'monthly';\n    l_cohort_count   number        := 12;\n    l_max_periods    number        := 12;\n\n    -- root\n    l_root           json_object_t := json_object_t();\n\n    -- cohorts[]\n    l_cohorts        json_array_t  := json_array_t();\n    l_cohort_obj     json_object_t;\n    l_cells          json_array_t;\n    l_cell           json_object_t;\n\n    -- periods[] / periodLabels[]\n    l_periods        json_array_t  := json_array_t();\n    l_period_labels  json_array_t  := json_array_t();\n\n    -- meta\n    l_meta_metric    chs_dataset_meta.metric%type;\n    l_meta_period    chs_dataset_meta.period_type%type;\n    l_generated_at   chs_dataset_meta.generated_at%type;\nbegin\n    -- ============================================================\n    -- Parse p_parameters per input_schema.json\n    -- (GetCohortDataInput: metric / periodType / cohortCount / maxPeriods)\n    -- All fields are optional; missing fields fall back to defaults.\n    -- ============================================================\n    if p_parameters is not null and dbms_lob.getlength(p_parameters) > 0 then\n        l_params := json_object_t.parse(p_parameters);\n\n        if l_params.has('metric') then\n            l_metric := l_params.get_string('metric');\n        end if;\n        if l_params.has('periodType') then\n            l_period_type := l_params.get_string('periodType');\n        end if;\n        if l_params.has('cohortCount') then\n            l_cohort_count := l_params.get_number('cohortCount');\n        end if;\n        if l_params.has('maxPeriods') then\n            l_max_periods := l_params.get_number('maxPeriods');\n        end if;\n    end if;\n\n    -- ============================================================\n    -- Validate against input_schema constraints\n    -- ============================================================\n    if l_metric not in ('retention', 'revenue', 'active') then\n        raise_application_error(-20002,\n            'metric must be one of: retention, revenue, active');\n    end if;\n    if l_period_type not in ('monthly', 'weekly') then\n        raise_application_error(-20003,\n            'periodType must be one of: monthly, weekly');\n    end if;\n    if l_cohort_count < 3 or l_cohort_count > 24 then\n        raise_application_error(-20004,\n            'cohortCount must be between 3 and 24');\n    end if;\n    if l_max_periods < 3 or l_max_periods > 24 then\n        raise_application_error(-20005,\n            'maxPeriods must be between 3 and 24');\n    end if;\n\n    -- ============================================================\n    -- Re-generate the dataset with the requested parameters.\n    -- chs_generate_cohort_data commits internally.\n    -- ============================================================\n    chs_generate_cohort_data(\n        p_metric       => l_metric,\n        p_period_type  => l_period_type,\n        p_cohort_count => l_cohort_count,\n        p_max_periods  => l_max_periods\n    );\n\n    -- ============================================================\n    -- meta: metric / periodType / generatedAt\n    -- ============================================================\n    select metric, period_type, generated_at\n    into   l_meta_metric, l_meta_period, l_generated_at\n    from   chs_dataset_meta\n    fetch first 1 rows only;\n\n    -- ============================================================\n    -- periods[] and periodLabels[]\n    -- ============================================================\n    for r in (\n        select period_code, period_label\n        from   chs_periods\n        order  by period_index\n    ) loop\n        l_periods.append(r.period_code);\n        l_period_labels.append(r.period_label);\n    end loop;\n\n    -- ============================================================\n    -- cohorts[] (with nested cells[])\n    -- ============================================================\n    for ch in (\n        select cohort_index, cohort_id, cohort_label, original_users\n        from   chs_cohorts\n        order  by cohort_index\n    ) loop\n        l_cohort_obj := json_object_t();\n        l_cells      := json_array_t();\n\n        for cell in (\n            select cohort_index, period_index, retention,\n                   users_retained, users_original\n            from   chs_cohort_cells\n            where  cohort_index = ch.cohort_index\n            order  by period_index\n        ) loop\n            l_cell := json_object_t();\n            l_cell.put('cohortIndex',   cell.cohort_index);\n            l_cell.put('periodIndex',   cell.period_index);\n            l_cell.put('retention',     cell.retention);\n            l_cell.put('usersRetained', cell.users_retained);\n            l_cell.put('usersOriginal', cell.users_original);\n            l_cells.append(l_cell);\n        end loop;\n\n        l_cohort_obj.put('cohortId',      ch.cohort_id);\n        l_cohort_obj.put('cohortLabel',   ch.cohort_label);\n        l_cohort_obj.put('originalUsers', ch.original_users);\n        l_cohort_obj.put('cells',         l_cells);\n        l_cohorts.append(l_cohort_obj);\n    end loop;\n\n    -- ============================================================\n    -- CohortDataSchema (root)\n    -- ============================================================\n    l_root.put('cohorts',      l_cohorts);\n    l_root.put('periods',      l_periods);\n    l_root.put('periodLabels', l_period_labels);\n    l_root.put('metric',       l_meta_metric);\n    l_root.put('periodType',   l_meta_period);\n    l_root.put(\n        'generatedAt',\n        to_char(\n            l_generated_at at time zone 'UTC',\n            'YYYY-MM-DD\"T\"HH24:MI:SS.FF3\"Z\"'\n        )\n    );\n\n    return l_root.to_clob();\nend chs_get_cohort_data_response;\n/\n"
}
U You 2026-04-17 01:26
✅ Result
The file /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql has been updated successfully.
C Claude 2026-04-17 01:26
💭 Thinking
C Claude 2026-04-17 01:26

[chs_get_cohort_data_response.sql](examples/cohort-heatmap-server/chs_get_cohort_data_response.sql) を改変しました。

変更点

  1. 1. <strong>シグネチャ</strong>: <code class="inline-code">function chs_get_cohort_data_response(p_parameters in clob default null) return clob</code>
  2. 2. <strong>JSONパース</strong>: <code class="inline-code">json_object_t.parse(p_parameters)</code> で受け取り、<code class="inline-code">has()</code> + <code class="inline-code">get_string()</code>/<code class="inline-code">get_number()</code> で各フィールド抽出。
  3. 3. <strong>デフォルト値</strong>: 各ローカル変数の初期値で <code class="inline-code">input_schema.json</code> の <code class="inline-code">default</code> を反映 (<code class="inline-code">retention</code> / <code class="inline-code">monthly</code> / <code class="inline-code">12</code> / <code class="inline-code">12</code>)。
  4. 4. <strong>バリデーション</strong>: <code class="inline-code">enum</code> と <code class="inline-code">min</code>/<code class="inline-code">max</code> を <code class="inline-code">raise_application_error</code> で検証。
  5. 5. <strong>再生成</strong>: 解析済みパラメータで <code class="inline-code">chs_generate_cohort_data</code> を呼び、テーブルを更新してから JSON を組み立て。

互換性

  • <code class="inline-code">default null</code> のため、引数なしで呼んでも従来通り動作します ([chs_install.sql](examples/cohort-heatmap-server/chs_install.sql) の <code class="inline-code">dbms_output.put_line(chs_get_cohort_data_response);</code> はそのまま使えます)。
  • 関数内で <code class="inline-code">chs_generate_cohort_data</code> (DML を含む) を呼ぶため、フレームワーク側は PL/SQL ブロック経由でこの関数を呼び出している前提です (SELECT 文から直接呼ぶ用途には向きません)。
U You 2026-04-17 01:32

chs_get_cohort_data_responseの現在の出力をstrucuredContentの値とし、typeがtextのtextとしてformatCohortSummaryもPL/SQLのコードで実装し、chs_get_cohort_data_responseの出力が以下の形式と一致するようにしてください。
return {
content: [{ type: "text", text: formatCohortSummary(data) }],
structuredContent: data,
};

C Claude 2026-04-17 01:36
💭 Thinking
C Claude 2026-04-17 01:36
chs_get_cohort_data_response{ content: [...], structuredContent: data } 形式の出力にし、formatCohortSummary を PL/SQL で実装しました。
C Claude 2026-04-17 01:36
Write
{
  "file_path": "/Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql",
  "content": "create or replace function chs_get_cohort_data_response(\n    p_parameters in clob default null\n) return clob\nas\n    -- ---------------------------------------------------------\n    -- Parsed inputs (defaults mirror input_schema.json)\n    -- ---------------------------------------------------------\n    l_params         json_object_t;\n    l_metric         varchar2(20)  := 'retention';\n    l_period_type    varchar2(20)  := 'monthly';\n    l_cohort_count   number        := 12;\n    l_max_periods    number        := 12;\n\n    -- root wrapper: { content: [...], structuredContent: data }\n    l_root           json_object_t := json_object_t();\n    l_content        json_array_t  := json_array_t();\n    l_content_item   json_object_t;\n\n    -- structuredContent = CohortData\n    l_data           json_object_t := json_object_t();\n\n    -- cohorts[]\n    l_cohorts        json_array_t  := json_array_t();\n    l_cohort_obj     json_object_t;\n    l_cells          json_array_t;\n    l_cell           json_object_t;\n\n    -- periods[] / periodLabels[]\n    l_periods        json_array_t  := json_array_t();\n    l_period_labels  json_array_t  := json_array_t();\n\n    -- meta\n    l_meta_metric    chs_dataset_meta.metric%type;\n    l_meta_period    chs_dataset_meta.period_type%type;\n    l_generated_at   chs_dataset_meta.generated_at%type;\n\n    -- formatCohortSummary() locals\n    l_n_cohorts      number;\n    l_n_periods      number;\n    l_avg_retention  number;\n    l_pct_str        varchar2(50);\n    l_summary_text   varchar2(4000);\nbegin\n    -- ============================================================\n    -- Parse p_parameters per input_schema.json\n    -- (GetCohortDataInput: metric / periodType / cohortCount / maxPeriods)\n    -- All fields are optional; missing fields fall back to defaults.\n    -- ============================================================\n    if p_parameters is not null and dbms_lob.getlength(p_parameters) > 0 then\n        l_params := json_object_t.parse(p_parameters);\n\n        if l_params.has('metric') then\n            l_metric := l_params.get_string('metric');\n        end if;\n        if l_params.has('periodType') then\n            l_period_type := l_params.get_string('periodType');\n        end if;\n        if l_params.has('cohortCount') then\n            l_cohort_count := l_params.get_number('cohortCount');\n        end if;\n        if l_params.has('maxPeriods') then\n            l_max_periods := l_params.get_number('maxPeriods');\n        end if;\n    end if;\n\n    -- ============================================================\n    -- Validate against input_schema constraints\n    -- ============================================================\n    if l_metric not in ('retention', 'revenue', 'active') then\n        raise_application_error(-20002,\n            'metric must be one of: retention, revenue, active');\n    end if;\n    if l_period_type not in ('monthly', 'weekly') then\n        raise_application_error(-20003,\n            'periodType must be one of: monthly, weekly');\n    end if;\n    if l_cohort_count < 3 or l_cohort_count > 24 then\n        raise_application_error(-20004,\n            'cohortCount must be between 3 and 24');\n    end if;\n    if l_max_periods < 3 or l_max_periods > 24 then\n        raise_application_error(-20005,\n            'maxPeriods must be between 3 and 24');\n    end if;\n\n    -- ============================================================\n    -- Re-generate the dataset with the requested parameters.\n    -- chs_generate_cohort_data commits internally.\n    -- ============================================================\n    chs_generate_cohort_data(\n        p_metric       => l_metric,\n        p_period_type  => l_period_type,\n        p_cohort_count => l_cohort_count,\n        p_max_periods  => l_max_periods\n    );\n\n    -- ============================================================\n    -- meta: metric / periodType / generatedAt\n    -- ============================================================\n    select metric, period_type, generated_at\n    into   l_meta_metric, l_meta_period, l_generated_at\n    from   chs_dataset_meta\n    fetch first 1 rows only;\n\n    -- ============================================================\n    -- periods[] and periodLabels[]\n    -- ============================================================\n    for r in (\n        select period_code, period_label\n        from   chs_periods\n        order  by period_index\n    ) loop\n        l_periods.append(r.period_code);\n        l_period_labels.append(r.period_label);\n    end loop;\n\n    -- ============================================================\n    -- cohorts[] (with nested cells[])\n    -- ============================================================\n    for ch in (\n        select cohort_index, cohort_id, cohort_label, original_users\n        from   chs_cohorts\n        order  by cohort_index\n    ) loop\n        l_cohort_obj := json_object_t();\n        l_cells      := json_array_t();\n\n        for cell in (\n            select cohort_index, period_index, retention,\n                   users_retained, users_original\n            from   chs_cohort_cells\n            where  cohort_index = ch.cohort_index\n            order  by period_index\n        ) loop\n            l_cell := json_object_t();\n            l_cell.put('cohortIndex',   cell.cohort_index);\n            l_cell.put('periodIndex',   cell.period_index);\n            l_cell.put('retention',     cell.retention);\n            l_cell.put('usersRetained', cell.users_retained);\n            l_cell.put('usersOriginal', cell.users_original);\n            l_cells.append(l_cell);\n        end loop;\n\n        l_cohort_obj.put('cohortId',      ch.cohort_id);\n        l_cohort_obj.put('cohortLabel',   ch.cohort_label);\n        l_cohort_obj.put('originalUsers', ch.original_users);\n        l_cohort_obj.put('cells',         l_cells);\n        l_cohorts.append(l_cohort_obj);\n    end loop;\n\n    -- ============================================================\n    -- Build structuredContent (CohortData)\n    -- ============================================================\n    l_data.put('cohorts',      l_cohorts);\n    l_data.put('periods',      l_periods);\n    l_data.put('periodLabels', l_period_labels);\n    l_data.put('metric',       l_meta_metric);\n    l_data.put('periodType',   l_meta_period);\n    l_data.put(\n        'generatedAt',\n        to_char(\n            l_generated_at at time zone 'UTC',\n            'YYYY-MM-DD\"T\"HH24:MI:SS.FF3\"Z\"'\n        )\n    );\n\n    -- ============================================================\n    -- formatCohortSummary(data)\n    -- Port of TypeScript:\n    --   const avgRetention = data.cohorts\n    --     .flatMap((c) => c.cells)\n    --     .filter((cell) => cell.periodIndex > 0)\n    --     .reduce((sum, cell, _, arr) => sum + cell.retention / arr.length, 0);\n    --   `Cohort Analysis: ${cohorts.length} cohorts, ${periods.length} periods\n    --    Average retention: ${(avgRetention * 100).toFixed(1)}%\n    --    Metric: ${metric}, Period: ${periodType}`\n    -- ============================================================\n    select count(*) into l_n_cohorts from chs_cohorts;\n    select count(*) into l_n_periods from chs_periods;\n    select nvl(avg(retention), 0)\n      into l_avg_retention\n      from chs_cohort_cells\n     where period_index > 0;\n\n    -- toFixed(1) equivalent: round to 1 decimal, force unit digit\n    l_pct_str := trim(to_char(round(l_avg_retention * 100, 1), '99990.0'));\n\n    l_summary_text :=\n        'Cohort Analysis: ' || l_n_cohorts || ' cohorts, '\n            || l_n_periods || ' periods' || chr(10) ||\n        'Average retention: ' || l_pct_str || '%' || chr(10) ||\n        'Metric: ' || l_meta_metric || ', Period: ' || l_meta_period;\n\n    -- ============================================================\n    -- content[0] = { type: \"text\", text: formatCohortSummary(data) }\n    -- ============================================================\n    l_content_item := json_object_t();\n    l_content_item.put('type', 'text');\n    l_content_item.put('text', l_summary_text);\n    l_content.append(l_content_item);\n\n    -- ============================================================\n    -- root = { content: [...], structuredContent: data }\n    -- ============================================================\n    l_root.put('content',           l_content);\n    l_root.put('structuredContent', l_data);\n\n    return l_root.to_clob();\nend chs_get_cohort_data_response;\n/\n"
}
U You 2026-04-17 01:36
✅ Result
The file /Users/********/Documents/mcp-app/examples/cohort-heatmap-server/chs_get_cohort_data_response.sql has been updated successfully.
C Claude 2026-04-17 01:37

[chs_get_cohort_data_response.sql](examples/cohort-heatmap-server/chs_get_cohort_data_response.sql) を更新しました。

変更点

  1. 1. <strong>ラッパー追加</strong>: 既存の CohortData JSON を <code class="inline-code">l_data</code> (= <code class="inline-code">structuredContent</code>) に詰め直し、新たな <code class="inline-code">l_root</code> を <code class="inline-code">{content: [...], structuredContent: l_data}</code> で構築。
  2. 2. <strong>formatCohortSummary を PL/SQL 化</strong>:
- l_n_cohorts / l_n_periodschs_cohorts / chs_periodscount(*) - l_avg_retentionchs_cohort_cells where period_index > 0avg(retention) (空集合時は nvl(..., 0) で TS の reduce(..., 0) と整合) - (x 100).toFixed(1)</code> … <code class="inline-code">to_char(round(x100, 1), '99990.0') + trim (例: 0.5"0.5") - 改行は chr(10) でテンプレートリテラルの実改行を再現
  1. 3. <strong>content 要素</strong>: <code class="inline-code">{type: &#x27;text&#x27;, text: l_summary_text}</code> を <code class="inline-code">l_content</code> に append。

出力イメージ

json
{
  "content": [
    {
      "type": "text",
      "text": "Cohort Analysis: 12 cohorts, 12 periods\nAverage retention: 45.3%\nMetric: retention, Period: monthly"
    }
  ],
  "structuredContent": {
    "cohorts": [...],
    "periods": ["M0","M1",...],
    "periodLabels": ["Month 0","Month 1",...],
    "metric": "retention",
    "periodType": "monthly",
    "generatedAt": "2026-04-17T12:34:56.789Z"
  }
}