---
title: "MCP 클라이언트용 OAuth"
description: "사용자를 TwexAPI에 로그인시키는 MCP 클라이언트를 만들기 위한 레퍼런스: 디스커버리, 동적 클라이언트 등록, PKCE, 스코프, 토큰, 갱신, 취소."
---

TwexAPI는 MCP 서버를 위한 OAuth 2.1 인가 서버를 운영합니다. Claude, ChatGPT, Cursor 같은 MCP 클라이언트는 이를 자동으로 사용합니다. 직접 클라이언트를 만들거나 클라이언트 문제를 디버깅할 때 이 페이지를 참고하세요.

- PKCE를 사용하는 인가 코드 흐름(`S256`만 지원).
- 퍼블릭 클라이언트만 지원하며 클라이언트 시크릿이 없습니다. 동적 클라이언트 등록(RFC 7591)으로 등록합니다.
- 사용자는 `https://twexapi.io/oauth/consent`에서 TwexAPI 계정으로 로그인하고 승인합니다.
- 토큰은 불투명한 문자열이며 `https://api.twexapi.io/mcp`에서만 동작합니다. TwexAPI API 키가 아니므로 REST API를 호출할 수 없습니다.
- 호출은 사용자의 API 키를 통해 해당 사용자의 크레딧에서 차감되므로, 승인하려면 크레딧이 남은 활성 키가 필요합니다.

스크립트나 백엔드에서 TwexAPI를 호출하기만 한다면 [API 키](/authentication)를 사용하세요.

## 디스커버리

자격 증명 없이 `https://api.twexapi.io/mcp`를 요청하면 다음 헤더와 함께 `401`이 반환됩니다.

```http
WWW-Authenticate: Bearer realm="twexapi", resource_metadata="https://api.twexapi.io/.well-known/oauth-protected-resource/mcp", scope="mcp:read mcp:write"
```

### 보호된 리소스 메타데이터

`GET https://api.twexapi.io/.well-known/oauth-protected-resource/mcp`(`/.well-known/oauth-protected-resource`에서도 같은 내용):

```json
{
  "resource": "https://api.twexapi.io/mcp",
  "authorization_servers": ["https://api.twexapi.io"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "resource_name": "TwexAPI"
}
```

### 인가 서버 메타데이터

`GET https://api.twexapi.io/.well-known/oauth-authorization-server`:

```json
{
  "issuer": "https://api.twexapi.io",
  "authorization_endpoint": "https://api.twexapi.io/oauth/authorize",
  "token_endpoint": "https://api.twexapi.io/oauth/token",
  "registration_endpoint": "https://api.twexapi.io/oauth/register",
  "revocation_endpoint": "https://api.twexapi.io/oauth/revoke",
  "response_types_supported": ["code"],
  "response_modes_supported": ["query"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "revocation_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "authorization_response_iss_parameter_supported": true
}
```

<a id="scopes"></a>

## 스코프

| 스코프 | 허용 범위 |
| --- | --- |
| `mcp:read` | `explore`와 `twexapi_request`(읽기 전용 엔드포인트). 항상 부여됩니다. |
| `mcp:write` | `twexapi_write`(트윗과 답글 게시). 사용자가 동의 페이지에서 거부할 수 있습니다. |

두 가지를 모두 요청하려면 `mcp:read mcp:write`를 지정합니다. 알 수 없는 스코프는 무시되며, 인식 가능한 스코프가 하나도 없으면 두 가지를 모두 요청한 것으로 처리합니다. 사용자가 실제로 허용한 범위는 토큰 응답의 `scope` 필드로 알 수 있습니다.

## 1. 클라이언트 등록

```http
POST https://api.twexapi.io/oauth/register
Content-Type: application/json

{
  "client_name": "My Agent",
  "redirect_uris": ["http://127.0.0.1:53682/callback"]
}
```

```json
{
  "client_id": "twx_client_...",
  "client_id_issued_at": 1792600000,
  "client_name": "My Agent",
  "redirect_uris": ["http://127.0.0.1:53682/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

| 필드 | 규칙 |
| --- | --- |
| `redirect_uris` | 필수. URI 1~10개이며 각각 다음 중 하나여야 합니다: `https://`, `localhost`·`127.0.0.1`·`[::1]`의 `http://`, 또는 `cursor://` 같은 사설 스킴. 프래그먼트나 사용자 정보는 허용되지 않습니다. |
| `client_name` | 선택. 최대 100자. 동의 페이지와 사용자의 **Authorized apps**(승인된 앱) 목록에 표시됩니다. |
| `token_endpoint_auth_method` | 선택. `none`만 허용됩니다. |
| `grant_types` | 선택. `authorization_code`와 `refresh_token`의 부분 집합. |
| `response_types` | 선택. `code`만 허용됩니다. |

등록은 IP 주소당 시간당 30개 클라이언트로 제한됩니다. 클라이언트 등록은 180일 동안 유효하며, 사용자가 해당 클라이언트로 인가를 시작할 때마다 연장됩니다. 한 번 등록한 뒤 `client_id`를 재사용하세요.

## 2. PKCE 쌍 만들기

Node.js:

```js
import crypto from "node:crypto";

const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
```

Python:

```python
import base64, hashlib, secrets

verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
```

verifier는 `A-Z a-z 0-9 - . _ ~`로 이루어진 43~128자여야 합니다.

## 3. 사용자를 인가 페이지로 보내기

사용자의 브라우저에서 다음 URL을 엽니다.

```txt
https://api.twexapi.io/oauth/authorize
  ?response_type=code
  &client_id=twx_client_...
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2Fcallback
  &code_challenge=CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_STATE
  &scope=mcp%3Aread%20mcp%3Awrite
  &resource=https%3A%2F%2Fapi.twexapi.io%2Fmcp
```

| 파라미터 | 필수 | 참고 |
| --- | --- | --- |
| `response_type` | 예 | `code`. |
| `client_id` | 예 | 등록 시 받은 값. |
| `redirect_uri` | 예 | 등록된 URI와 정확히 일치해야 합니다. 루프백 `http` URI는 어떤 포트든 사용할 수 있습니다. |
| `code_challenge`, `code_challenge_method` | 예 | `S256`만 지원합니다. |
| `state` | 권장 | 그대로 반환됩니다. CSRF 방지를 위해 검증하세요. |
| `scope` | 아니요 | [스코프](#scopes)를 참고하세요. |
| `resource` | 아니요 | 보내는 경우 `https://api.twexapi.io/mcp`여야 합니다. |

TwexAPI는 사용자를 `twexapi.io`의 동의 페이지로 보냅니다. 사용자는 로그인하고, 앱 이름과 돌아갈 호스트를 확인하고, 쓰기 권한 부여 여부를 선택한 뒤 승인하거나 거부합니다. 요청은 10분 후 만료됩니다.

## 4. 코드 받기

승인하면 브라우저가 리디렉션 URI로 돌아옵니다.

```txt
http://127.0.0.1:53682/callback?code=CODE&state=RANDOM_STATE&iss=https%3A%2F%2Fapi.twexapi.io
```

`state`가 일치하는지, `iss`가 `https://api.twexapi.io`인지 확인하세요. 거부하면 `code` 대신 `error=access_denied`가 전달됩니다. 코드는 한 번만 사용할 수 있으며 2분 후 만료됩니다.

`client_id`를 알 수 없거나 `redirect_uri`가 등록되지 않은 경우, TwexAPI는 리디렉션하지 않고 `400` 오류를 표시합니다. 따라서 잘못 설정된 클라이언트가 사용자를 검증되지 않은 주소로 보내는 일은 없습니다.

## 5. 코드를 토큰으로 교환

```http
POST https://api.twexapi.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=CODE
&redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2Fcallback
&client_id=twx_client_...
&code_verifier=VERIFIER
```

```json
{
  "access_token": "twx_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "twx_rt_...",
  "scope": "mcp:read mcp:write"
}
```

`redirect_uri`와 `client_id`는 인가 요청과 같아야 합니다. `resource=https://api.twexapi.io/mcp`를 함께 보낼 수도 있습니다. JSON 본문도 허용됩니다.

## 6. MCP 서버 호출

```http
POST https://api.twexapi.io/mcp
Authorization: Bearer twx_at_...
Content-Type: application/json
Accept: application/json, text/event-stream
```

액세스 토큰은 `/mcp`에서만 동작합니다. 만료되었거나 취소되었거나 알 수 없는 토큰에는 `WWW-Authenticate` 헤더에 `error="invalid_token"`이 포함된 `401`이 반환됩니다. 토큰을 갱신하거나 사용자에게 다시 로그인하도록 요청하세요.

## 토큰 갱신

```http
POST https://api.twexapi.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=twx_rt_...
&client_id=twx_client_...
```

갱신할 때마다 새 액세스 토큰과 새 리프레시 토큰이 반환되고, 이전 리프레시 토큰은 더 이상 동작하지 않습니다. 매번 새로 받은 쌍을 저장하세요. 이전 리프레시 토큰이 다시 사용되면 TwexAPI는 유출된 것으로 간주해 인가 전체를 취소합니다. 그 인가의 모든 토큰이 동작을 멈추고 사용자는 다시 로그인해야 합니다.

## 취소

```http
POST https://api.twexapi.io/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=twx_rt_...
```

리프레시 토큰을 취소하면 인가 전체가 종료됩니다. 액세스 토큰을 취소하면 해당 토큰만 무효가 됩니다. 이 엔드포인트는 알 수 없는 토큰에도 항상 `200`을 반환합니다. 사용자는 [대시보드](https://twexapi.io/dashboard)의 **Authorized apps**(승인된 앱)에서도 앱을 취소할 수 있으며, 즉시 적용됩니다.

## 토큰 유효 기간

| 항목 | 유효 기간 |
| --- | --- |
| 인가 요청(동의 페이지) | 10분 |
| 인가 코드 | 2분, 1회용 |
| 액세스 토큰 | 1시간 |
| 리프레시 토큰 | 30일, 사용할 때마다 교체 |
| 30일 동안 사용되지 않은 인가 | 만료 |
| 클라이언트 등록 | 180일, 인가할 때마다 연장 |

## 오류

오류는 표준 OAuth 형식을 따릅니다.

```json
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid, expired or already used"
}
```

| 엔드포인트 | `error` | 원인 |
| --- | --- | --- |
| 등록 | `invalid_redirect_uri` | 리디렉션 URI가 없거나 형식이 잘못되었거나, 허용되지 않는 스킴이나 호스트를 사용합니다. |
| 등록 | `invalid_client_metadata` | 지원하지 않는 `token_endpoint_auth_method`, `grant_types`, `response_types`. |
| 등록 | `temporarily_unavailable`(`429`) | 해당 IP 주소의 등록이 너무 많습니다. `client_id`를 재사용하세요. |
| 인가 | `invalid_client`(`400`, 리디렉션 없음) | 알 수 없는 `client_id`입니다. 먼저 등록하세요. |
| 인가 | `invalid_request`(`400`, 리디렉션 없음) | `redirect_uri`가 이 클라이언트에 등록되지 않았습니다. |
| 인가 | `unsupported_response_type` | `response_type`이 `code`가 아닙니다. |
| 인가 | `invalid_request` | PKCE 파라미터가 없거나 잘못되었습니다. |
| 인가 | `invalid_target` | `resource`가 `https://api.twexapi.io/mcp`가 아닙니다. |
| 인가 | `access_denied` | 사용자가 접근을 거부했습니다. |
| 토큰 | `invalid_grant` | 코드나 리프레시 토큰이 잘못되었거나, 만료되었거나, 이미 사용되었거나, 다른 클라이언트에 발급된 것입니다. 또는 PKCE 검증에 실패했거나 인가가 취소되었습니다. |
| 토큰 | `invalid_target` | `resource`가 일치하지 않습니다. |
| 토큰 | `unsupported_grant_type` | `authorization_code` 또는 `refresh_token`을 사용하세요. |

사용자 계정에 활성 API 키나 크레딧이 없으면 동의 페이지가 승인 전에 막기 때문에, 호출할 수 없는 토큰이 클라이언트에 전달되는 일은 없습니다.
