# 다운 KITE MCP 사용 안내

다운(Dawoon) 농장 관리 시스템 **KITE** 의 데이터를 MCP(Model Context Protocol)로 조회하고, 업무 도구로 등록·수정·삭제하는 서버입니다.
AI 에이전트(Claude Code, Claude Desktop, Cursor, Codex 등)나 개발자 프로그램이 API 키로 인증한 뒤 사용합니다.

| 항목 | 값 |
|---|---|
| MCP 주소 | `https://mcp.dawoon.link/mcp` |
| 전송 방식 | Streamable HTTP · **stateless** · `POST` 만 사용 · JSON 응답 |
| 인증 | `Authorization: Bearer dwm_xxxxxxxx_...` (또는 `X-API-Key: dwm_...`) |
| REST API | `https://mcp.dawoon.link/api/v1` (앱·농장 PC용, 아래 REST API 절) |
| 상태 확인 | `https://mcp.dawoon.link/healthz` (인증 불필요) |
| 이 문서 원문 | `https://mcp.dawoon.link/docs/USAGE.md` · `https://mcp.dawoon.link/docs/llms.txt` |

이 키로 제공되는 범위는 아래 4개 테이블입니다.

| 테이블 | 내용 | 행 수(약) |
|---|---|---|
| `tblCompany` | 회사·농장 정보 | 970 |
| `tblUser` | 사용자 계정 | 1,170 |
| `tblAnimals` | 개체(가축) 기본 정보 | 3만 4천 |
| `tblFarmEnv` | 농장 환경 측정값 | **1,393만** |

## AGENT QUICKSTART

AI 에이전트는 아래 순서를 그대로 따르면 됩니다.

1. `get_guide` 로 이 안내를 읽는다(이미 읽었다면 생략). 일부만 필요하면 `get_guide { "section": "register_user" }` 처럼 절 제목으로 거른다.
2. `describe_table` 로 쓸 테이블의 컬럼·타입·기본키를 확인한다. 컬럼 이름은 추측하지 않는다.
3. 농장 이름만 알면 먼저 `select_rows` 로 `tblCompany` 에서 `compNm like '%이름%'` 을 찾아 `compCd`(목장 코드)를 얻는다.
4. `select_rows` 는 **항상 `compCd` 조건**을 넣고 `limit` 은 작게(10~100) 시작한다. `tblFarmEnv` 는 반드시 `compCd` + `envDt` 날짜 범위를 함께 준다.
5. **데이터 변경은 업무 도구 9개로만** 한다: `register_farm`·`update_farm`·`delete_farm`·`register_user`·`update_user`·`delete_user`·`update_animal`·`delete_animal`·`record_farm_env`. 테이블을 직접 고치거나 프로시저를 직접 실행하는 도구는 없다.
6. 수정 도구는 **준 항목만 바꾸고 나머지는 그대로 둔다**. 현재 값을 다시 채워 넘길 필요가 없다.
7. 삭제는 `flagYN='N'` **표시 삭제**다. 되살리려면 `update_*` 에 `active: true` 를 준다. 조회할 때는 보통 `flagYN eq 'Y'` 조건을 같이 준다.
8. 사용자에게 무엇을 바꿀지 먼저 확인받고 변경 도구를 호출한다. 오류 문장은 한국어로 원인을 알려 주니 그대로 읽고 인자를 고친다.

## REST API (앱·농장 PC)

MCP 와 같은 기능을 일반 HTTP(REST) 로도 제공합니다. 앱·웹 서버·농장 PC 처럼 AI 가 아닌 프로그램은 REST 를 쓰세요. 검증 규칙·권한·감사 기록은 MCP 와 완전히 같습니다.

| 항목 | 값 |
|---|---|
| 기본 주소 | `https://mcp.dawoon.link/api/v1` |
| 인증 | `Authorization: Bearer dwm_xxxxxxxx_...` (또는 `X-API-Key: dwm_...`) — MCP 와 같은 키 |
| 본문 형식 | 요청·응답 모두 `application/json; charset=utf-8` |
| API 문서(시험 호출 가능) | https://mcp.dawoon.link/docs/api/ |
| OpenAPI 명세 | `https://mcp.dawoon.link/api/v1/openapi.json` |

정확한 경로·입력 스키마는 위 API 문서(OpenAPI)가 기준입니다. 아래 표는 요약입니다.

### 응답 형식

성공하면 `{"data": ...}`, 실패하면 `{"error": {"status": 404, "message": "목장 compCd=1 이(가) 없습니다."}}` 를 돌려줍니다. 오류 문장은 한국어로 원인을 알려 줍니다.

| 상태 코드 | 뜻 | 프로그램이 할 일 |
|---|---|---|
| 200 / 201 | 성공 (201 = 새로 만듦) | `data` 사용 |
| 400 | 입력값 오류 (필수값 누락·형식·길이 초과 등) | 메시지대로 고친 뒤 다시 보내기. 그대로 재시도해도 같음 |
| 401 | 키가 없거나 틀림·폐기·만료 | 키 확인. 재시도 금지 |
| 403 | 이 키로는 안 되는 작업 (권한·농장 제한) | 관리자에게 권한 요청 |
| 404 | 대상이 없음 | 코드·아이디 확인 |
| 409 | 이미 있음 / 이미 삭제됨 (중복) | 기존 데이터 확인 |
| 429 | 요청이 너무 많음 (키당 분당 제한·동시 실행 2개) | 잠시 기다렸다가 재시도 |
| 500 | 서버·DB 오류 | 잠시 뒤 재시도, 계속되면 관리자에게 시각과 경로 전달 |

### 경로 요약

| 메서드 | 경로 | 내용 | 권한 |
|---|---|---|---|
| GET | `/farms?q=이름&limit=&offset=` | 목장 목록 | read |
| GET | `/farms/{compCd}` | 목장 1건 | read |
| POST | `/farms` | 목장 등록 (= register_farm) | write |
| PATCH | `/farms/{compCd}` | 목장 수정 (준 항목만) | write |
| DELETE | `/farms/{compCd}` | 목장 사용 중지 | write |
| GET | `/users?compCd=` | 사용자 목록 | read |
| GET | `/users/{userID}` | 사용자 1명 | read |
| POST | `/users` | 사용자 등록 | write |
| PATCH | `/users/{userID}` | 사용자 수정 (비밀번호 생략 시 유지) | write |
| DELETE | `/users/{userID}` | 사용자 정지 | write |
| GET | `/farms/{compCd}/animals` | 개체 목록 | read |
| GET | `/farms/{compCd}/animals/{aniSeq}` | 개체 1마리 | read |
| PATCH | `/farms/{compCd}/animals/{aniSeq}` | 개체 수정 | write |
| DELETE | `/farms/{compCd}/animals/{aniSeq}` | 개체 삭제 표시 | write |
| GET | `/farms/{compCd}/env?from=YYYY-MM-DD&to=YYYY-MM-DD` | 환경 측정값 조회 | read |
| POST | `/farms/{compCd}/env` | 환경 측정값 기록 (여러 건 한 번에) | write |
| GET | `/tables` · `/tables/{table}` | 테이블 목록·구조 | read |
| POST | `/tables/{table}/query` · `/tables/{table}/count` | 조건 조회·개수 (본문은 select_rows 와 같음) | read |
| GET | `/procedures` · `/procedures/{name}` | 프로시저 목록·파라미터 (참고용) | read |
| GET | `/guide` | 이 안내 문서 (마크다운) | read |

- 등록·수정 본문의 필드는 아래 "변경 도구 레퍼런스" 의 같은 이름 도구 입력과 같습니다. 예: `POST /farms` 본문 = `register_farm` 입력.
- 목록은 `limit`(기본 100, 최대 1000)·`offset` 으로 나눠 받고, 응답의 `hasMore` 가 `true` 면 `offset` 을 늘려 다음 쪽을 받습니다.
- 농장 제한 키는 자기 농장 `compCd` 만 다룰 수 있고, 다른 농장은 403 입니다.

### 앱(모바일·웹)에서 쓸 때

- 이 API 는 **최종 사용자 로그인 기능을 제공하지 않습니다.** 앱 사용자 인증은 앱 쪽 서버가 맡고, 그 서버가 이 API 를 부르는 구조(서버 간 호출)를 권장합니다.
- **배포하는 모바일 앱 안에 쓰기(write) 키를 넣지 마세요.** 앱 파일은 누구나 풀어 볼 수 있어 키가 새어 나갑니다. 꼭 앱에서 직접 불러야 하면 조회(read) 전용 + 해당 농장 제한 키를 따로 발급받으세요.
- 브라우저(웹 앱)에서 직접 부를 때는 출처(CORS) 허용 등록이 필요합니다. 웹 주소를 관리자에게 알려 주세요. 가능하면 웹 서버를 거쳐 부르세요.
- 키가 새어 나갔다고 의심되면 바로 관리자에게 알려 폐기·재발급합니다.

### 농장 PC에서 쓸 때

- **농장마다 키를 따로** 받습니다: 쓰기(write) 권한 + 그 농장 `compCd` 제한. 다른 농장 데이터는 이 키로 건드릴 수 없습니다.
- 환경 측정값은 `POST /farms/{compCd}/env` 에 `records` 배열로 한 번에 보냅니다(최대 50건, 한 트랜잭션).
- 기록 시각은 **서버가 현재 시각(시 단위)으로 정합니다.** PC 시계와 무관하며, 같은 시간대에 같은 항목을 다시 보내면 마지막 값으로 덮어씁니다. 그래서 같은 요청을 재시도해도 중복 행이 생기지 않습니다. 지난 시간대 값은 나중에 보낼 수 없으니 측정 즉시 보내세요.
- 재시도 규칙: 429 는 1~5초 기다렸다 재시도, 500·네트워크 오류는 1·2·4·8·16초 간격으로 최대 5번 재시도, 400·401·403·404 는 재시도하지 않고 로그에 남깁니다.

curl:

```bash
curl -s -X POST https://mcp.dawoon.link/api/v1/farms/3321/env \
  -H "Authorization: Bearer $KITE_KEY" -H "Content-Type: application/json" \
  -d '{"records":[{"envPosit":"1","envCd":"T","envVal":"23.8"},{"envPosit":"1","envCd":"H","envVal":"61"}]}'

curl -s "https://mcp.dawoon.link/api/v1/farms/3321/animals?limit=50" -H "Authorization: Bearer $KITE_KEY"
```

C# (.NET HttpClient) — 농장 PC:

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://mcp.dawoon.link/api/v1/") };
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("KITE_KEY"));

// 환경값 여러 건 기록 — 500·429·네트워크 오류는 지수 백오프로 최대 5번
async Task PostEnvAsync(int compCd, object[] records)
{
    for (var attempt = 0; ; attempt++)
    {
        try
        {
            var res = await http.PostAsJsonAsync($"farms/{compCd}/env", new { records });
            if (res.IsSuccessStatusCode) return;
            var code = (int)res.StatusCode;
            if ((code != 429 && code < 500) || attempt >= 4)
                throw new Exception($"{code}: {await res.Content.ReadAsStringAsync()}");
        }
        catch (HttpRequestException) when (attempt < 4) { }
        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
    }
}

await PostEnvAsync(3321, new object[] {
    new { envPosit = "1", envCd = "T", envVal = "23.8" },
    new { envPosit = "1", envCd = "H", envVal = "61" },
});

// 개체 목록
var json = await http.GetStringAsync("farms/3321/animals?limit=100");
```

Kotlin (OkHttp) — 앱 서버·안드로이드(조회 키):

```kotlin
val client = OkHttpClient()
val req = Request.Builder()
    .url("https://mcp.dawoon.link/api/v1/farms/3321/animals?limit=50")
    .header("Authorization", "Bearer $kiteReadKey")
    .build()
client.newCall(req).execute().use { res ->
    val body = res.body!!.string()          // {"data": {"rows": [...], "hasMore": false, ...}}
    if (!res.isSuccessful) error("KITE ${res.code}: $body")
}
```

Swift (URLSession):

```swift
var req = URLRequest(url: URL(string: "https://mcp.dawoon.link/api/v1/farms/3321/animals?limit=50")!)
req.setValue("Bearer \(kiteReadKey)", forHTTPHeaderField: "Authorization")
let (data, res) = try await URLSession.shared.data(for: req)
guard (res as? HTTPURLResponse)?.statusCode == 200 else { throw KiteError.http(data) }
```

## 키 발급

- 키는 **서버개발자(관리자)** 에게 요청합니다.
- 키 형식: `dwm_<8자 식별자>_<40자>`. 원문은 발급 순간 한 번만 보여 주며 서버에는 해시만 저장됩니다.
- 키마다 **권한(scope)**, **농장 제한(compCd 목록)**, **만료일**, **분당 요청 수**가 정해져 있습니다.
- 키를 잃어버렸거나 노출됐으면 바로 관리자에게 알려 폐기하고 새로 받으세요.

| scope | 쓸 수 있는 도구 |
|---|---|
| `read` (조회) | get_guide, list_tables, describe_table, select_rows, count_rows, list_procedures, describe_procedure |
| `write` (조회·변경) | read 의 모든 도구 + register_farm, update_farm, delete_farm, register_user, update_user, delete_user, update_animal, delete_animal, record_farm_env |

키 권한에 없는 도구는 `tools/list` 에 아예 나타나지 않습니다.

## 연결 방법

### Claude Code

```bash
claude mcp add --transport http dawoon-kite https://mcp.dawoon.link/mcp \
  --header "Authorization: Bearer dwm_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

### Claude Desktop · Cursor 등 JSON 설정

원격 HTTP 서버를 지원하는 클라이언트는 아래 형태로 넣습니다. (키 이름은 클라이언트마다 조금 다르니 해당 문서를 확인하세요)

```json
{
  "mcpServers": {
    "dawoon-kite": {
      "type": "http",
      "url": "https://mcp.dawoon.link/mcp",
      "headers": { "Authorization": "Bearer dwm_xxxxxxxx_..." }
    }
  }
}
```

원격 HTTP 를 직접 지원하지 않는 클라이언트는 `mcp-remote` 같은 브리지를 씁니다.

```json
{
  "mcpServers": {
    "dawoon-kite": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.dawoon.link/mcp", "--header", "Authorization:Bearer ${DAWOON_MCP_KEY}"],
      "env": { "DAWOON_MCP_KEY": "dwm_xxxxxxxx_..." }
    }
  }
}
```

### Codex CLI

`~/.codex/config.toml` 에 추가합니다. (Codex 버전에 따라 설정 키가 다를 수 있으니 `codex mcp --help` 로 확인하세요)

```toml
[mcp_servers.dawoon-kite]
url = "https://mcp.dawoon.link/mcp"
bearer_token_env_var = "DAWOON_MCP_KEY"
```

```bash
export DAWOON_MCP_KEY="dwm_xxxxxxxx_..."
```

### curl 로 직접 호출 (개발자용)

stateless 서버라 세션 ID 없이 요청마다 바로 호출할 수 있습니다. `Accept` 헤더에 두 형식을 모두 넣어야 합니다.

```bash
KEY="dwm_xxxxxxxx_..."
URL="https://mcp.dawoon.link/mcp"
H=(-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")

# 1) initialize
curl -s "$URL" "${H[@]}" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

# 2) 도구 목록
curl -s "$URL" "${H[@]}" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 3) 조회 — 농장 3321 의 개체 5마리
curl -s "$URL" "${H[@]}" -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"select_rows","arguments":{"table":"tblAnimals","where":[{"column":"compCd","op":"eq","value":3321},{"column":"flagYN","value":"Y"}],"limit":5}}}'
```

결과는 `result.content[0].text` 에 JSON 문자열로 들어 있습니다. 도구 오류는 `result.isError: true` 와 함께 `오류: ...` 문장으로 옵니다.

## 도구 레퍼런스

### 공통: where 조건

`where` 는 아래 객체의 배열이며 모두 **AND** 로 묶입니다. 컬럼 이름은 대소문자를 가리지 않습니다.

```json
{ "column": "compCd", "op": "eq", "value": 3321 }
```

| op | 의미 | 값 |
|---|---|---|
| `eq` `ne` | = , <> | `value` |
| `lt` `lte` `gt` `gte` | < , <= , > , >= | `value` |
| `like` | LIKE (`%` 직접 넣기) | `value` (예: `"%한우%"`) |
| `in` | IN | `values`: 배열(최대 1000개) |
| `between` | BETWEEN | `values`: `[시작, 끝]` |
| `isnull` `notnull` | IS NULL / IS NOT NULL | 없음 |

`op` 를 생략하면 `eq` 입니다. NULL 비교는 `eq` 대신 `isnull` 을 씁니다.
값은 모두 파라미터로 바인딩되며 컬럼 선언 타입(varchar/int/datetime …)으로 전달됩니다.
datetime 컬럼에는 `"2026-10-20 13:00:00"` 같은 문자열을 줍니다(한국 시간 기준). 결과의 날짜도 같은 형식으로 나옵니다.

### get_guide · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `section` | string? | 제목에 이 문자열이 들어간 절만 돌려받기 (예: `"register_user"`) |

이 키로 볼 수 있는 사용 안내(이 문서)를 마크다운으로 돌려줍니다.

### list_tables · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `search` | string? | 이름에 포함된 문자열 |

제공되는 테이블 목록과 통계상 행 수, 기본키, 표시 삭제(`flagYN`) 여부를 돌려줍니다.

### describe_table · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `table` | string | 테이블 이름 (`dbo.` 생략 가능) |

컬럼 이름·타입·NULL 허용·자동 증가(identity)·민감 여부(`sensitive`)와 기본키를 돌려줍니다.

### select_rows · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `table` | string | 테이블 |
| `columns` | string[]? | 생략하면 전체 컬럼 |
| `where` | Filter[]? | 위 조건 |
| `orderBy` | `{column, desc?}[]`? | 생략하면 기본키 순 |
| `limit` | int 1~1000, 기본 100 | 가져올 행 수 |
| `offset` | int ≥ 0, 기본 0 | 건너뛸 행 수 (페이지 넘기기) |

결과: `{ count, offset, hasMore, rows }`. `hasMore` 가 `true` 면 `offset` 을 늘려 다음 쪽을 가져옵니다.

### count_rows · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `table` | string | |
| `where` | Filter[]? | |

결과: `{ count }`. `tblFarmEnv` 를 조건 없이 세면 느립니다. 대략적인 수는 `list_tables` 의 `rows` 를 보세요.

### list_procedures · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `search` | string? | 이름 일부 |

이 범위의 프로시저 이름과 파라미터 목록을 **참고용**으로 돌려줍니다. 프로시저를 직접 실행하는 도구는 없으며, 데이터 변경은 아래 "변경 도구"로 합니다(도구가 내부에서 해당 프로시저를 사이트와 같은 방식으로 부릅니다).

### describe_procedure · read

| 인자 | 타입 | 설명 |
|---|---|---|
| `name` | string | 예: `sp_User` |

파라미터(이름·타입·OUTPUT 여부·기본값 유무)를 참고용으로 돌려줍니다.

## 변경 도구 레퍼런스

`write` 키에만 보입니다. 모든 변경 도구는 공통으로:

- 한 번의 트랜잭션 안에서 **존재·중복 확인 → 기존 값 병합 → 저장 → 저장 결과 다시 읽기** 순서로 동작하고, 중간에 실패하면 아무것도 바뀌지 않습니다.
- 수정 도구는 **준 항목만 바꿉니다.** 주지 않은 항목은 지금 값이 그대로 유지됩니다.
- 글자 길이는 **바이트**로 검사합니다(한글 1자 = 2바이트). 넘으면 잘라서 저장하지 않고 오류로 알려 줍니다. 예: 대표자·담당자 최대 한글 5자, 사용자 이름 최대 한글 10자, 목장명 최대 한글 25자.
- 저장자(`issueID`)는 `mcp_<키 식별자 8자>` 로 자동 기록됩니다.
- 농장 제한 키는 허용된 `compCd` 에만 쓸 수 있고, 새 목장 등록은 할 수 없습니다.
- 사용 중지(`flagYN` 이 `Y` 가 아닌) 목장에는 사용자 등록·개체 수정·환경값 기록을 할 수 없습니다.

| 도구 | 대상 | 하는 일 |
|---|---|---|
| `register_farm` | tblCompany | 목장 등록 → 새 `compCd` |
| `update_farm` | tblCompany | 목장 정보 수정·사용 재개/중지 |
| `delete_farm` | tblCompany | 목장 사용 중지 |
| `register_user` | tblUser | 로그인 사용자 등록 |
| `update_user` | tblUser | 사용자 정보·비밀번호·권한 수정, 재개/정지 |
| `delete_user` | tblUser | 사용자 정지 |
| `update_animal` | tblAnimals | 개체 정보 수정·삭제 표시/복구 |
| `delete_animal` | tblAnimals | 개체 삭제 표시 |
| `record_farm_env` | tblFarmEnv | 현재 시각의 환경 측정값 기록 |

### 코드표

**구분 `part`** (목장 종류)

| 코드 | 뜻 |
|---|---|
| `00300` | 농장 (기본값) |
| `00100` | 사료회사 |
| `00200` | 사료 대리점 |
| `00400` | TMR |
| `00900` | 기타 |
| `00500` | API 연결사 — `apiKey` 를 비우면 숫자 키를 자동으로 만듭니다 |

**등급 `level`** (사용자)

| 코드 | 뜻 |
|---|---|
| `00100` | 일반 (기본값) |
| `00050` | 열람 전용 |
| `00900` | 관리자 |

**메뉴 권한 `perms` / `mPerms`** — 콤마로 이은 메뉴 코드 목록(또는 배열)입니다. 없는 코드는 거부됩니다.

| 구분 | 기본값 (생략하면 이 값으로 등록) |
|---|---|
| `perms` (웹) | `A07,A01,A02,A05,A04,A06,A10,A13,A15,A16,A03,A09` |
| `mPerms` (모바일) | `M02,M03,M04,M05,M07,M08,M09,M10,M11,M15` |

- `A07` = `dash.html` (대시보드, 기본 시작 페이지), `A10` = `controlFarm.html` (농장 제어).
- **시작 페이지 `startPage` 는 그 메뉴 코드가 `perms` 안에 있어야** 합니다(사이트 규칙). 예: `startPage: "controlFarm.html"` 이면 `perms` 에 `A10` 이 있어야 함.
- 기존 사용자의 권한 목록을 바꿀 때 현재 시작 페이지 메뉴가 빠지면 거부합니다 — `startPage` 도 함께 바꾸세요.

### register_farm · write

새 목장(거래처)을 등록합니다. **목장 기본 설정이 함께 만들어지며**, 만들어진 것을 확인한 뒤 성공으로 돌려줍니다.

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compNm` | string | 예 | 목장명 (최대 50바이트, 한글 25자) |
| `part` | string | 아니오 (기본 `00300`) | 구분 코드 (위 코드표) |
| `owner` | string | 아니오 | 대표자 (최대 한글 5자) |
| `phone` | string | 아니오 | 전화 |
| `fax` | string | 아니오 | 팩스 |
| `major` | string | 아니오 | 담당자 (최대 한글 5자) |
| `majorPhone` | string | 아니오 | 담당자 연락처 (SMS 수신처, 콤마 구분) |
| `address` | string | 아니오 | 주소 (최대 한글 100자) |
| `compNo` | string | 아니오 | 사업자등록번호 |
| `memo` | string | 아니오 | 메모 (최대 한글 500자) |
| `testTerms` | `YYYY-MM-DD` | 아니오 (기본 **오늘+14일**) | 체험(사용) 기한 |
| `apiKey` | string | 아니오 | API 연결사(`00500`) 키. `00500` 인데 비우면 자동 생성 |
| `allowDuplicateName` | boolean | 아니오 (기본 false) | 같은 이름의 사용 중인 목장이 있어도 등록 |

거부되는 경우: 같은 이름의 **사용 중인** 목장이 이미 있을 때(`allowDuplicateName: true` 가 아니면 — 오류에 기존 `compCd` 를 알려 줌), 없는 `part` 코드, 바이트 길이 초과, 농장 제한 키.

응답: `{ compCd, farm: {저장된 행}, note? }` — **`compCd` 가 새 목장 코드**입니다. `farm.apiKey` 는 `***` 로 가려집니다.

```json
{ "compNm": "행복목장", "owner": "홍길동", "phone": "031-xxx-xxxx", "major": "김담당",
  "majorPhone": "010-xxxx-xxxx", "address": "경기도 ...", "compNo": "000-00-00000" }
```

### update_farm · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compCd` | int | 예 | 수정할 목장 코드 |
| `compNm` · `part` · `owner` · `phone` · `fax` · `major` · `majorPhone` · `address` · `compNo` · `memo` · `testTerms` · `apiKey` | | 아니오 | 바꿀 항목만 (규칙은 register_farm 과 같음) |
| `active` | boolean | 아니오 | `true` 사용 재개(`flagYN='Y'`), `false` 사용 중지 |

없는 `compCd` 는 거부합니다(새로 만들지 않음). 응답: `{ compCd, changed: [바뀐 항목], farm }`.

```json
{ "compCd": 3321, "majorPhone": "010-xxxx-xxxx", "memo": "담당자 변경" }
```

### delete_farm · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compCd` | int | 예 | 목장 코드 |

목장을 **사용 중지**(`flagYN='N'`)합니다. 데이터는 지우지 않으며 `update_farm { compCd, active: true }` 로 되돌립니다. 사용 중지된 목장의 사용자는 로그인할 수 없습니다. 이미 중지된 목장이면 오류. 응답: `{ compCd, flagYN: "N" }`.

### register_user · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `userID` | string | 예 | 아이디 — 영문·숫자·`._@-` 2~20자 |
| `compCd` | int | 예 | 소속 목장 (사용 중이어야 함) |
| `password` | string | 예 | 비밀번호 평문 **4~50자** — 서버에서 암호화 저장 |
| `userNm` | string | 예 | 이름 (최대 한글 10자) |
| `phone` | string | 아니오 | 전화 |
| `email` | string | 아니오 | 이메일 |
| `level` | string | 아니오 (기본 `00100`) | 등급 코드 |
| `perms` | string[] 또는 콤마 문자열 | 아니오 (기본: 위 기본 권한) | 웹 메뉴 권한 |
| `mPerms` | string[] 또는 콤마 문자열 | 아니오 (기본: 위 기본 권한) | 모바일 메뉴 권한 |
| `startPage` | string | 아니오 (기본 `dash.html`) | 로그인 후 첫 화면. 그 메뉴가 `perms` 에 있어야 함 |
| `phoneGroup` | string | 아니오 | 푸시 메시지 그룹 |
| `phoneCd` | string | 아니오 | 휴대폰(푸시) 코드 |
| `smsYN` | `Y`/`N` | 아니오 (기본 `Y`) | 문자 수신 여부 |

거부되는 경우: **아이디 중복 — 삭제(정지)된 아이디도 다시 쓸 수 없습니다**, 비밀번호 4자 미만·50자 초과, 없거나 사용 중지된 목장, 없는 등급·메뉴 코드, 시작 페이지 메뉴가 `perms` 에 없음, 바이트 길이 초과.

응답: `{ userID, user: {저장된 행} }` (`passwd` 는 `***`). 저장 뒤 비밀번호가 제대로 들어갔는지까지 확인합니다.

```json
{ "userID": "happy02", "compCd": 3321, "password": "초기비밀번호", "userNm": "김직원", "phone": "010-xxxx-xxxx" }
```

### update_user · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `userID` | string | 예 | 수정할 아이디 |
| `compCd` | int | 아니오 | 소속 목장 변경 (사용 중인 목장만) |
| `password` | string | 아니오 | 새 비밀번호 4~50자. **생략하면 기존 비밀번호 유지** |
| `userNm` · `phone` · `email` · `level` · `perms` · `mPerms` · `startPage` · `phoneGroup` · `phoneCd` · `smsYN` | | 아니오 | 바꿀 항목만 (규칙은 register_user 와 같음) |
| `active` | boolean | 아니오 | `true` 사용 재개, `false` 정지 |

응답: `{ userID, changed: [바뀐 항목], user }`.

```json
{ "userID": "happy02", "perms": ["A07","A01","A10"], "startPage": "controlFarm.html" }
```

### delete_user · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `userID` | string | 예 | 아이디 |

사용자를 **정지**(`flagYN='N'`)합니다. 로그인할 수 없게 되며, 아이디는 다른 사람이 다시 쓸 수 없습니다. 비밀번호는 보존되므로 `update_user { userID, active: true }` 로 되돌리면 같은 비밀번호로 로그인할 수 있습니다. 응답: `{ userID, flagYN: "N" }`.

### update_animal · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compCd` | int | 예 | 목장 코드 |
| `aniSeq` | int | 예 | 개체 일련번호 |
| `changes` | `{컬럼명: 값}` | 예 (`active` 만 줄 때는 `{}`) | 바꿀 항목. 이름은 `tblAnimals` 컬럼명 (예: `aniNick`, `aniGroupCd`, `aniBirthday`, `aniSex`, `aniMemo`, `aniFeedSet1`~`10`, `buyCost`, `aniType`) |
| `active` | boolean | 아니오 | `true` 사용, `false` 삭제 표시 |

- `compCd`·`aniSeq`·`flagYN`·`issueID` 는 `changes` 로 바꿀 수 없습니다.
- 날짜 항목(`aniBirthday`, `aniFeedSupDate`, `aniDayAddStart`)은 `YYYY-MM-DD`.
- 주지 않은 항목(사료 설정량 6~10, 사진 경로, 발정 기기 코드 등 포함)은 모두 그대로 유지됩니다.
- 없는 개체·사용 중지된 목장은 거부합니다.

응답: `{ changed: [바뀐 항목], animal: {저장된 행} }`.

```json
{ "compCd": 3321, "aniSeq": 121, "changes": { "aniNick": "복순이", "aniMemo": "왼쪽 뒷다리 치료 중" } }
```

**새 개체 등록은 제공하지 않습니다.** 개체 번호(`aniSeq`) 발급과 RFID·이표번호 중복 확인은 농장 PC 동기화가 맡고 있어서, 여기서 새로 만들면 농장 PC 와 어긋납니다. 새 개체는 농장 PC 에서 등록하세요.

### delete_animal · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compCd` | int | 예 | 목장 코드 |
| `aniSeq` | int | 예 | 개체 일련번호 |

개체를 삭제 표시(`flagYN='N'`)합니다. `update_animal { compCd, aniSeq, changes: {}, active: true }` 로 되돌립니다. 응답: `{ compCd, aniSeq, flagYN: "N" }`.

### record_farm_env · write

| 인자 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `compCd` | int | 예 | 목장 코드 (사용 중이어야 함) |
| `envPosit` | string (1~5자) | 예 | 측정 장소 코드 (예: `1`) |
| `envCd` | string (1~5자) | 예 | 환경 요소 코드 (예: `T` 온도, `H` 습도) |
| `envVal` | string 또는 number | 예 | 측정값 (최대 10자) |
| `groupCd` | string (최대 4자) | 아니오 (기본 `C003`) | 장소 그룹 |

**기록 시각은 서버의 현재 날짜·현재 시각(정시 단위)** 으로 정해집니다. 과거 시각으로는 기록할 수 없고, 같은 시간대에 다시 기록하면 그 값을 덮어씁니다. 응답: `{ recorded: true, compCd, envPosit, envCd, envVal, at }`.

```json
{ "compCd": 3321, "envPosit": "1", "envCd": "T", "envVal": "23.8" }
```

## 안전 규칙과 오류

- **변경은 업무 도구로만**: 테이블을 직접 추가·수정·삭제하거나 프로시저를 직접 실행하는 도구는 없습니다. 위 9개 변경 도구가 사이트와 같은 방식으로 처리합니다.
- **표시 삭제**: 삭제는 `flagYN='N'` 으로 표시만 합니다. 행은 남아 있고 `active: true` 로 되돌릴 수 있습니다.
- **민감 컬럼**: `tblUser.passwd`, `tblCompany.apiKey` 는 조회 결과에서 `***` 로 가려집니다.
- **농장 제한 키**: 모든 조회·실행에 허용된 `compCd` 만 쓰입니다. 다른 농장 값은 거부됩니다.
- **요청 제한**: 키마다 분당 요청 수(기본 120회), 그리고 IP 마다 초당 10회(순간 40회)까지 받습니다. 요청당 시간 제한은 30초입니다.
- **감사 기록**: 모든 도구 호출이 키·시각·인자(민감 값은 `***`)·결과와 함께 기록되고 관리자가 봅니다.
- 키를 수정·폐기하면 최대 30초 안에 반영됩니다.

| HTTP | 의미 | 할 일 |
|---|---|---|
| 401 | 키가 없거나 틀림·만료·폐기 | 키 확인, 관리자에게 문의 |
| 429 | 요청 제한 초과 | 잠시 뒤 다시 시도, 호출 횟수 줄이기 |
| 405 | GET/DELETE 요청 | `POST` 만 사용 |
| 503 | 서버가 DB 에 연결 못 함 | 잠시 뒤 다시 시도, 계속되면 관리자에게 |
| 200 + `isError` | 도구 오류(조건·권한·데이터) | 오류 문장대로 인자 수정 |

## 데이터 구조

- DB: SQL Server, 정렬 `Korean_Wansung_CI_AS`.
- **`compCd` = 회사/농장 코드**. `tblCompany` 의 기본키이고 나머지 테이블을 농장별로 묶는 열쇠입니다.
- 날짜·생일·측정일은 대부분 **문자열**입니다: `'YYYY-MM-DD'`, 시각은 `'HH:MM'`. 문자열 비교로 범위 조회가 됩니다(`between ["2026-10-01","2026-10-20"]`).
- `regDate`·`issueDate` 는 datetime(등록/수정 시각), `issueID` 는 마지막 저장자 ID.
- `flagYN`: `'Y'` 유효, `'N'` 삭제됨(표시 삭제).
- 코드 값(`part`, `aniGroupCd`, `envPosit`, `envCd` 등)은 KITE 의 공통 코드 번호입니다. 의미를 모르면 같은 농장의 기존 행에서 쓰인 값을 따라 씁니다.

### tblCompany — 회사·농장

기본키 `compCd` (자동 증가). 약 970행.

| 컬럼 | 타입 | 설명 |
|---|---|---|
| compCd | int, identity | 회사(농장) 코드 |
| compNm | varchar(50) | 회사명 |
| owner | varchar(10) | 대표자 |
| phone | varchar(50) | 연락처 |
| fax | varchar(20) | FAX |
| major | varchar(20) | 담당자 이름 |
| majorPhone | varchar(100) | 담당자 연락처 (SMS 수신처, 쉼표로 구분) |
| address | varchar(200) | 주소 |
| compNo | varchar(20) | 사업자 번호 |
| part | varchar(5) | 구분 (사료회사·농장 등, 공통 코드. 농장은 `00300`) |
| memo | varchar(1000) | 메모 |
| apiKey | varchar(50) | API 키 — **민감, `***` 로 가려짐** |
| testTerms | varchar(10) | 테스트 기간 |
| flagYN | varchar(1) | 유효 여부 (Y 유효 / N 정지·삭제) |
| regDate | datetime | 등록일 |
| issueDate | datetime | 최종 수정일 |
| issueID | varchar(20) | 최종 저장자 ID |

### tblUser — 사용자

기본키 `userID`. 약 1,170행. 농장별로 보려면 `compCd` 조건을 줍니다.

| 컬럼 | 타입 | 설명 |
|---|---|---|
| userID | varchar(20) | 사용자 아이디 |
| compCd | int | 소속 회사(농장) 코드 |
| passwd | nvarchar(200) | 비밀번호(암호화 저장) — **민감, `***` 로 가려짐** |
| userNm | varchar(20) | 이름 |
| phone | varchar(20) | 연락처 |
| email | varchar(100) | 이메일 |
| level | varchar(50) | 등급 |
| perms | varchar(1000) | 권한 |
| mPerms | varchar(1000) | 모바일 권한 |
| agreeUseInfo | varchar(1) | 개인정보 이용 동의 (Y/N) |
| phoneCd | varchar(50) | 스마트폰 기기 코드 |
| androidKey | varchar(1000) | 스마트폰 푸시 키 |
| appVersion | varchar(10) | 앱 버전 |
| phoneGroup | varchar(20) | 푸시 메시지 발송 그룹 |
| startPage | varchar(20) | 로그인 시작 페이지 |
| smsYN | varchar(1) | SMS 수신 여부 (Y/N) |
| flagYN | varchar(1) | 가용 여부 (Y 유효 / N 삭제) |
| regDate | datetime | 최초 저장일 |
| issueDate | datetime | 최종 저장일 |
| issueID | varchar(20) | 최종 저장자 |

### tblAnimals — 개체(가축)

기본키 `(compCd, aniSeq)`. 약 3만 4천 행. `aniSeq` 는 농장 안에서의 개체 일련번호이며 **자동으로 매겨지지 않습니다** — 새 개체는 같은 `compCd` 의 가장 큰 `aniSeq` + 1 을 씁니다.

| 컬럼 | 타입 | 설명 |
|---|---|---|
| compCd | int | 회사(농장) 코드 |
| aniSeq | smallint | 개체 일련번호 |
| aniOwnerNo | varchar(20) | 농장이 부여한 개체 번호 |
| aniRFID | varchar(20) | RFID |
| aniNationalID | varchar(20) | 이표번호(국가 부여) |
| aniBleCd | smallint | BLE 코드 |
| aniNick | varchar(20) | 별명 |
| aniGroupCd | varchar(5) | 그룹 번호 (공통 코드) |
| aniBirthday | varchar(10) | 생년월일 `YYYY-MM-DD` |
| aniSex | varchar(1) | 성별 (M 수컷 / F 암컷) |
| aniFeedSet1 ~ aniFeedSet10 | numeric(10,0) | 사료 설정량 1~10 |
| aniFeedSetLast1 ~ aniFeedSetLast10 | numeric(10,0) | 최종 목표 사료량 1~10 |
| aniFeedMethod | varchar(5) | 급여 방법 (공통 코드) |
| aniFeedSupply | varchar(5) | 사료 공급 방법 (공통 코드) |
| aniFeedSupDate | varchar(10) | 기기 사용 시작일(입적일) |
| aniImageFilePath | varchar(100) | 사진 파일 경로 |
| aniFeedCount | varchar(5) | 일일 급여 횟수/방법 (공통 코드) |
| aniDayAddStart | varchar(10) | 증가/감소 급여 시작 허용일 |
| aniLapsedDays | varchar(5) | 증가/감소 급여 경과 일수 |
| aniFeedDevice | varchar(5) | 기기 코드 (급이기·포유기 등, 공통 코드) |
| aniMemo | varchar(4000) | 특이사항·메모 |
| deviceType | varchar(5) | 장비 타입 |
| deviceCd | smallint | 장비 코드 |
| heatDevCd | int | 발정 관리 기기 코드 |
| updateFlag | varchar(1) | 장비 반영 여부 (N 반영 전 / Y 반영 완료) |
| buyCost | numeric(10,0) | 구입 단가 |
| aniType | varchar(5) | 가축 등급 (공통 코드) |
| flagYN | varchar(1) | 가용 여부 (Y 유효 / N 삭제) |
| regDate | datetime | 최초 저장일 |
| issueDate | datetime | 최종 저장일 |
| issueID | varchar(20) | 최종 저장자 |

### tblFarmEnv — 농장 환경 측정값

기본키 `(compCd, envPosit, envCd, envDt, stdTm, groupCd)`. **약 1,393만 행** — 반드시 `compCd` 와 `envDt` 범위를 같이 주고 `limit` 을 작게 시작합니다. 기본키가 `compCd` 로 시작하므로 `compCd` 조건이 있으면 빠르게 찾습니다.

| 컬럼 | 타입 | 설명 |
|---|---|---|
| compCd | int | 회사(농장) 코드 |
| envPosit | varchar(5) | 측정 장소 (공통 코드) |
| envCd | varchar(5) | 환경 요소 코드 (온도·습도 등, 공통 코드) |
| envDt | varchar(10) | 측정 날짜 `YYYY-MM-DD` |
| stdTm | varchar(5) | 기준 시각 `HH:00` (1시간 단위) |
| groupCd | varchar(4) | 장소 그룹 코드 (기본 `C003`) |
| envVal | varchar(10) | 측정값 (문자열) |
| flagYN | varchar(1) | 상태 (Y 접수 / C 처리 완료 / N 삭제) |
| regDate | datetime | 최초 저장일 |
| issueDate | datetime | 최종 저장일 |
| issueID | varchar(20) | 최종 저장자 |

조회 예:

```json
{ "table": "tblFarmEnv",
  "where": [ {"column":"compCd","value":3321},
             {"column":"envDt","op":"between","values":["2026-10-19","2026-10-20"]} ],
  "orderBy": [ {"column":"envDt","desc":true}, {"column":"stdTm","desc":true} ],
  "limit": 50 }
```

## 권장 작업 흐름

1. **구조 보기** — `describe_table { "table": "tblUser" }`
2. **목장 코드 찾기** — `select_rows { "table": "tblCompany", "columns": ["compCd","compNm","flagYN"], "where": [{"column":"compNm","op":"like","value":"%행복%"}], "limit": 10 }`
3. **좁혀서 조회** — `select_rows { "table": "tblUser", "where": [{"column":"compCd","value":3321},{"column":"flagYN","value":"Y"}], "limit": 50 }`
4. **사용자 확인** — 무엇을 어떻게 바꿀지 보여 주고 확인받는다.
5. **변경** — 알맞은 변경 도구 호출 (바꿀 항목만).
6. **결과 확인** — 응답에 담긴 저장된 행을 사용자에게 보여 준다.

## 예시 질문 (AI 에이전트에게)

- "행복목장의 compCd 와 담당자 연락처를 알려줘."
- "3321 농장의 유효한 개체 목록을 이표번호와 생년월일로 정리해줘."
- "3321 농장의 어제 시간대별 온도(envCd T) 측정값을 보여줘."
- "새 목장 '행복목장'을 등록하고 대표 홍길동으로 사용자 happy01 을 만들어줘." (write 키 필요, 저장 전에 확인을 받아요)
- "happy02 사용자의 시작 화면을 농장 제어로 바꿔줘." (write 키 필요)
- "3321 농장 121번 개체의 별명을 복순이로 고쳐줘." (write 키 필요)

문의: 서버개발자 (MCP 서버·키 관리 담당)
