edu-workspace-mcp 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -21
- package/dist/auth/google-auth.d.ts +11 -0
- package/dist/auth/google-auth.js +118 -19
- package/dist/auth/google-auth.js.map +1 -1
- package/dist/cli.js +52 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/google/classroom.d.ts +134 -3
- package/dist/google/classroom.js +172 -6
- package/dist/google/classroom.js.map +1 -1
- package/dist/google/docs.d.ts +33 -0
- package/dist/google/docs.js +189 -8
- package/dist/google/docs.js.map +1 -1
- package/dist/google/drive.d.ts +1 -0
- package/dist/google/drive.js +21 -9
- package/dist/google/drive.js.map +1 -1
- package/dist/google/education-sheets.d.ts +84 -0
- package/dist/google/education-sheets.js +606 -0
- package/dist/google/education-sheets.js.map +1 -0
- package/dist/google/forms.d.ts +69 -0
- package/dist/google/forms.js +128 -3
- package/dist/google/forms.js.map +1 -1
- package/dist/google/references.d.ts +2 -0
- package/dist/google/references.js +31 -0
- package/dist/google/references.js.map +1 -0
- package/dist/google/retry.d.ts +10 -0
- package/dist/google/retry.js +42 -0
- package/dist/google/retry.js.map +1 -0
- package/dist/google/sheets-inspection.d.ts +155 -0
- package/dist/google/sheets-inspection.js +222 -0
- package/dist/google/sheets-inspection.js.map +1 -0
- package/dist/google/sheets.d.ts +30 -0
- package/dist/google/sheets.js +183 -6
- package/dist/google/sheets.js.map +1 -1
- package/dist/google/slides.d.ts +21 -0
- package/dist/google/slides.js +131 -10
- package/dist/google/slides.js.map +1 -1
- package/dist/install/configure-client.d.ts +9 -0
- package/dist/install/configure-client.js +136 -0
- package/dist/install/configure-client.js.map +1 -0
- package/dist/server.d.ts +21 -6
- package/dist/server.js +334 -21
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +31 -11
package/README.md
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
자연어로 Google Docs, Sheets, Slides, Forms, Drive와 Classroom을 사용하는 교육 특화 Model Context Protocol(MCP) 서버입니다. Apps Script 없이 Google 공식 API를 직접 호출합니다.
|
|
4
4
|
|
|
5
|
-
[공식 홈페이지](https://edu.jeld.kr/) · [개인정보처리방침](https://edu.jeld.kr/privacy.html) · [이용약관](https://edu.jeld.kr/terms.html)
|
|
5
|
+
[공식 홈페이지](https://edu.jeld.kr/) · [복사 가능한 예시 프롬프트](https://edu.jeld.kr/#prompts) · [개인정보처리방침](https://edu.jeld.kr/privacy.html) · [이용약관](https://edu.jeld.kr/terms.html)
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> `1.0.0`은 로컬 stdio MCP 정식 안정 버전입니다. 공용 Google 데스크톱 OAuth 클라이언트가 포함되어 있어 최종 사용자가 Google Cloud 프로젝트를 만들 필요가 없습니다. 기본 모드는 앱이 다루는 파일에 한정된 `drive.file`을 사용하며, 기존 자료 읽기는 사용자가 `--read`로 선택합니다.
|
|
8
8
|
|
|
9
9
|
## 할 수 있는 일
|
|
10
10
|
|
|
11
11
|
- 수업 자료용 Google Docs 문서 생성
|
|
12
|
-
- 학생
|
|
12
|
+
- 학생 명단·평가계획·평가기록·제출현황·관찰기록·대시보드가 연결된 Google Sheets 생성
|
|
13
|
+
- 셀 값과 수식 본문을 노출하지 않는 기존 Sheets 구조·수식 오류 진단
|
|
14
|
+
- Classroom 명단·제출 상태를 개인정보 최소화 시트로 변환
|
|
13
15
|
- 차시별 Google Slides 프레젠테이션 생성
|
|
14
16
|
- 정답·배점이 포함된 Google Forms 퀴즈 생성
|
|
17
|
+
- 기존 Docs·Sheets·Slides·Forms 내용과 Forms 응답 읽기
|
|
15
18
|
- Google Drive 파일 검색, 폴더 생성, 승인 후 공유
|
|
16
|
-
- 교사가 담당하는
|
|
19
|
+
- 교사가 담당하는 Classroom 수업·과제·학생 명단·제출 현황 조회
|
|
17
20
|
- Classroom 과제 초안 생성, 사용자 승인 후 게시
|
|
18
21
|
|
|
19
22
|
예시 요청:
|
|
@@ -36,22 +39,25 @@
|
|
|
36
39
|
|
|
37
40
|
대표 데모 프롬프트:
|
|
38
41
|
|
|
39
|
-
> 5학년 과학 ‘소화와 순환’ 수업 패키지를 만들어줘. Drive에 전용 폴더를 만들고, 교사용 수업안과 학생용 학습지는
|
|
42
|
+
> 5학년 과학 ‘소화와 순환’ 40분 수업 패키지를 만들어줘. Drive에 전용 폴더를 만들고, 교사용 수업안과 학생용 학습지는 제목·소제목·본문의 위계와 충분한 답안 공간이 보이게 Docs로 만들어줘. Slides는 한 장에 핵심 메시지 하나씩 7장으로, Forms는 정답·배점을 포함한 5문항으로 구성해줘. 모든 자료에서 학습 목표와 용어를 동일하게 쓰고 정보량을 점검해줘. ‘5학년 3반 과학’ Classroom에는 학생용 자료만 과제 초안으로 연결하고 수업·마감·배점·첨부를 보여준 뒤 게시 전에 확인받아줘.
|
|
40
43
|
|
|
41
44
|
| 교육 업무 | 만들어지는 결과 | 사용하는 도구 |
|
|
42
45
|
| --- | --- | --- |
|
|
43
46
|
| 한 차시 수업 패키지 | 수업안, 학습지, 슬라이드, 퀴즈, Classroom 초안 | Drive + Docs + Slides + Forms + Classroom |
|
|
44
|
-
| 과정중심평가 관리 |
|
|
47
|
+
| 과정중심평가 관리 | 9개 연결 탭, 드롭다운, 체크박스, 조건부 서식, 차트 | Sheets |
|
|
48
|
+
| Classroom 제출 관리 | 명단·제출 상태·점수·대시보드 스냅샷 | Classroom + Sheets |
|
|
49
|
+
| 복잡한 업무 시트 점검 | 탭·수식 함수·의존성·오류·차트 구조 진단 | Sheets |
|
|
45
50
|
| 수행평가 설계 | 4수준 루브릭 문서와 학생 자기평가지 | Docs + Forms |
|
|
46
51
|
| 형성평가 배포 | 정답·배점이 있는 퀴즈와 과제 초안 | Forms + Classroom |
|
|
47
52
|
| 학기 자료 정리 | 검색 결과와 교과·단원별 Drive 폴더 | Drive |
|
|
48
53
|
| 동료 교사 협업 | 확인 절차를 거친 댓글·읽기·편집 권한 | Drive 공유 |
|
|
49
54
|
|
|
50
|
-
- [
|
|
55
|
+
- [서비스별로 복사 가능한 예시 프롬프트](https://edu.jeld.kr/#prompts)
|
|
56
|
+
- [교육자를 위한 실제 활용 예시와 도구 흐름](docs/EDUCATOR_USE_CASES.md)
|
|
51
57
|
- [예제 모음과 추천 첫 데모](examples/README.md)
|
|
52
58
|
- [실제로 실행되는 수업 패키지 통합 테스트](src/tests/education-demo.test.ts)
|
|
53
59
|
|
|
54
|
-
>
|
|
60
|
+
> 읽기 확장 모드에서는 기존 자료를 조회·분석할 수 있습니다. 교육용 평가·제출 템플릿에는 차트·드롭다운·조건부 서식이 포함됩니다. 기존 문서나 임의 범위의 범용 부분 편집은 아직 지원하지 않습니다. 자세한 지원 범위는 [활용 예시 문서](docs/EDUCATOR_USE_CASES.md#현재-버전에서-가능한-범위)를 확인하세요.
|
|
55
61
|
|
|
56
62
|
## MCP 도구
|
|
57
63
|
|
|
@@ -59,12 +65,25 @@
|
|
|
59
65
|
| --- | --- | --- |
|
|
60
66
|
| `workspace_get_auth_status` | Google 연결 상태와 범위 확인 | 읽기 |
|
|
61
67
|
| `classroom_list_courses` | 교사가 담당하는 활성 수업 조회 | 읽기 |
|
|
62
|
-
| `
|
|
68
|
+
| `classroom_list_coursework` | 수업의 과제·자료·마감·배점 조회 | 읽기 |
|
|
69
|
+
| `classroom_list_students` | 수업 학생 명단 조회 | 읽기 |
|
|
70
|
+
| `classroom_list_student_submissions` | 과제별 제출 상태·점수·답변 조회 | 읽기 |
|
|
71
|
+
| `drive_search_files` | 앱이 접근 가능한 파일을 이름·유형·폴더로 검색 | 읽기 |
|
|
72
|
+
| `drive_get_file_metadata` | Drive 파일 정보와 권한 상태 조회 | 읽기 |
|
|
63
73
|
| `drive_create_folder` | Drive 폴더 생성 | 생성 |
|
|
64
74
|
| `docs_create_document` | Docs 문서 생성 | 생성 |
|
|
75
|
+
| `docs_read_document` | Docs 본문·표·모든 문서 탭 읽기 | 읽기 |
|
|
65
76
|
| `sheets_create_workbook` | 여러 탭과 초기 데이터가 있는 Sheets 생성 | 생성 |
|
|
77
|
+
| `sheets_list_sheets` | 스프레드시트의 탭 목록과 크기 확인 | 읽기 |
|
|
78
|
+
| `sheets_read_values` | 스프레드시트 값 읽기 (범위 지정, 행 수 제한) | 읽기 |
|
|
79
|
+
| `sheets_inspect_workbook` | 셀 값을 반환하지 않고 구조·수식·오류·의존성 진단 | 읽기 |
|
|
80
|
+
| `education_create_assessment_tracker` | 9개 탭이 연결된 과정중심평가 시스템 생성 | 생성 |
|
|
81
|
+
| `education_create_classroom_submission_tracker` | Classroom 과제 제출 현황 대시보드 시트 생성 | 생성 |
|
|
66
82
|
| `slides_create_presentation` | 제목·본문 슬라이드 생성 | 생성 |
|
|
83
|
+
| `slides_read_presentation` | 슬라이드별 텍스트·표·발표자 노트 읽기 | 읽기 |
|
|
67
84
|
| `forms_create_quiz` | 문항·정답·배점이 있는 퀴즈 생성 | 생성 |
|
|
85
|
+
| `forms_read_form` | 설문 구조·문항·선택지·정답 읽기 | 읽기 |
|
|
86
|
+
| `forms_list_responses` | 제출된 설문 응답·점수 읽기 | 읽기 |
|
|
68
87
|
| `classroom_create_assignment_draft` | 학생에게 보이지 않는 과제 초안 생성 | 생성 |
|
|
69
88
|
| `classroom_publish_assignment` | 승인된 과제 초안을 학생에게 게시 | 외부 변경 |
|
|
70
89
|
| `drive_prepare_share` | 공유 대상·권한 승인 준비 | 승인 준비 |
|
|
@@ -72,6 +91,52 @@
|
|
|
72
91
|
|
|
73
92
|
게시와 공유 도구에는 MCP 안전 메타데이터가 포함되어 있습니다. 승인 ID는 15분 동안 유효하고 한 번만 사용할 수 있습니다.
|
|
74
93
|
|
|
94
|
+
### 기존 Google 자료를 읽으려면 (선택)
|
|
95
|
+
|
|
96
|
+
기본 범위인 `drive.file` 은 **이 앱이 만들었거나 사용자가 이 앱으로 연 파일만** 다룹니다.
|
|
97
|
+
그래서 예전부터 쓰던 Docs·Sheets·Slides·Forms는 기본 설정으로는 읽히지 않을 수 있습니다.
|
|
98
|
+
|
|
99
|
+
기존 자료와 Classroom 학생 정보를 읽으려면 설치와 로그인에 `--read`를 붙이세요. 서비스별 읽기 전용 범위는 Google에서 민감 범위로 분류하므로 기본으로 요청하지 않습니다.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx -y edu-workspace-mcp@1.0.0 install claude --read
|
|
103
|
+
npx -y edu-workspace-mcp@1.0.0 login --read
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
MCP 클라이언트 설정에 넣을 때는 `env` 에 적습니다.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"edu-workspace": {
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": ["-y", "edu-workspace-mcp"],
|
|
114
|
+
"env": { "EDU_WORKSPACE_READ_ACCESS": "1" }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
껐다 켰다 하면 요청 범위가 달라지므로, 바꾼 뒤에는 **반드시 다시 로그인**해야 합니다.
|
|
121
|
+
`workspace_get_auth_status` 로 지금 허용된 범위를 확인할 수 있습니다.
|
|
122
|
+
|
|
123
|
+
`claude`는 Claude Code의 사용자 범위에 등록되어 모든 프로젝트에서 동작합니다. Claude Desktop은 `install claude-desktop --read`를 사용하세요. 사용 예:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
sheets_list_sheets { "spreadsheet": "https://docs.google.com/spreadsheets/d/<ID>/edit" }
|
|
127
|
+
sheets_read_values { "spreadsheet": "<ID>", "range": "'AI 피드백'!A1:J40", "maxRows": 40 }
|
|
128
|
+
sheets_inspect_workbook { "spreadsheet": "<ID>", "includeHidden": true }
|
|
129
|
+
education_create_assessment_tracker { "title": "5학년 평가 관리", "className": "5학년 3반", "schoolYear": 2026, "semester": "2학기", "students": [{ "number": 1, "name": "학생01" }] }
|
|
130
|
+
docs_read_document { "document": "https://docs.google.com/document/d/<ID>/edit" }
|
|
131
|
+
forms_list_responses { "form": "https://docs.google.com/forms/d/<ID>/edit", "maxResponses": 100 }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `spreadsheet` 는 ID 와 전체 URL 을 모두 받습니다.
|
|
135
|
+
- `range` 를 비우면 첫 시트를 읽습니다.
|
|
136
|
+
- 값은 화면에 보이는 대로 읽으므로 수식과 `IMPORTRANGE` 결과도 그대로 들어옵니다. 원본 값이 필요하면 `raw: true`.
|
|
137
|
+
- 기본 200행까지만 돌려주고(`maxRows` 로 최대 2,000), 잘렸으면 `truncated: true` 로 알려 줍니다.
|
|
138
|
+
- 개인정보가 있는 복잡한 시트의 설계만 점검할 때는 `sheets_inspect_workbook`을 사용하세요. 셀 값과 수식 본문은 반환하지 않고 함수명 통계·시트 의존성·오류 위치만 알려 줍니다.
|
|
139
|
+
|
|
75
140
|
## 요구 사항
|
|
76
141
|
|
|
77
142
|
- Node.js 20 이상
|
|
@@ -80,23 +145,33 @@
|
|
|
80
145
|
|
|
81
146
|
## 빠른 설치 — 최종 사용자
|
|
82
147
|
|
|
83
|
-
|
|
148
|
+
macOS, Windows, Linux에서 사용할 AI 클라이언트를 한 번만 전체 프로젝트 설정에 등록합니다. Google Cloud Console 설정은 필요하지 않습니다.
|
|
84
149
|
|
|
85
150
|
```bash
|
|
86
|
-
|
|
87
|
-
npx -y edu-workspace-mcp
|
|
151
|
+
# 하나만 선택
|
|
152
|
+
npx -y edu-workspace-mcp@1.0.0 install codex
|
|
153
|
+
npx -y edu-workspace-mcp@1.0.0 install claude # Claude Code, 사용자 범위
|
|
154
|
+
npx -y edu-workspace-mcp@1.0.0 install claude-desktop
|
|
155
|
+
npx -y edu-workspace-mcp@1.0.0 install cursor
|
|
156
|
+
|
|
157
|
+
# 사용자별·컴퓨터별 최초 1회 Google 연결
|
|
158
|
+
npx -y edu-workspace-mcp@1.0.0 login
|
|
88
159
|
```
|
|
89
160
|
|
|
90
|
-
|
|
161
|
+
기존 Google 자료까지 읽을 사용자는 두 명령 모두에 `--read`를 붙입니다.
|
|
162
|
+
|
|
163
|
+
AI 클라이언트를 재시작한 뒤 새 프로젝트에서도 같은 MCP를 사용할 수 있습니다. 브라우저에서 Google 로그인과 권한 승인을 마치면 됩니다. 학교 계정에서 기관 관리자가 제3자 앱을 차단한 경우에는 관리자에게 앱 허용을 요청해야 합니다.
|
|
164
|
+
|
|
165
|
+
- [학교 Google Workspace 관리자 허용 안내](docs/ADMIN_GUIDE.md)
|
|
91
166
|
|
|
92
167
|
```bash
|
|
93
|
-
npx -y edu-workspace-mcp doctor
|
|
168
|
+
npx -y edu-workspace-mcp@1.0.0 doctor
|
|
94
169
|
```
|
|
95
170
|
|
|
96
|
-
Google 토큰은 `~/.edu-workspace-mcp/token.json`에 사용자 전용 권한으로 저장됩니다. 연결을
|
|
171
|
+
Google 토큰은 `~/.edu-workspace-mcp/token.json`에 사용자 전용 권한으로 저장됩니다. 연결을 해제하면 Google 측 권한 철회와 로컬 토큰 삭제를 함께 수행합니다.
|
|
97
172
|
|
|
98
173
|
```bash
|
|
99
|
-
npx -y edu-workspace-mcp disconnect
|
|
174
|
+
npx -y edu-workspace-mcp@1.0.0 disconnect
|
|
100
175
|
```
|
|
101
176
|
|
|
102
177
|
## MCP 클라이언트 연결
|
|
@@ -108,7 +183,7 @@ Claude Desktop과 Cursor 계열 JSON 설정:
|
|
|
108
183
|
"mcpServers": {
|
|
109
184
|
"edu-workspace": {
|
|
110
185
|
"command": "npx",
|
|
111
|
-
"args": ["-y", "edu-workspace-mcp"]
|
|
186
|
+
"args": ["-y", "edu-workspace-mcp@1.0.0"]
|
|
112
187
|
}
|
|
113
188
|
}
|
|
114
189
|
}
|
|
@@ -117,9 +192,9 @@ Claude Desktop과 Cursor 계열 JSON 설정:
|
|
|
117
192
|
Codex·ChatGPT Desktop 계열 TOML 설정:
|
|
118
193
|
|
|
119
194
|
```toml
|
|
120
|
-
[mcp_servers.
|
|
195
|
+
[mcp_servers.edu-workspace]
|
|
121
196
|
command = "npx"
|
|
122
|
-
args = ["-y", "edu-workspace-mcp"]
|
|
197
|
+
args = ["-y", "edu-workspace-mcp@1.0.0"]
|
|
123
198
|
default_tools_approval_mode = "writes"
|
|
124
199
|
```
|
|
125
200
|
|
|
@@ -143,13 +218,16 @@ node dist/cli.js login
|
|
|
143
218
|
## 보안 모델
|
|
144
219
|
|
|
145
220
|
- 필요한 Google OAuth 범위만 요청합니다.
|
|
221
|
+
- OAuth 로그인에 PKCE(S256), 무작위 `state`, 5분 만료와 loopback 콜백 검증을 적용합니다.
|
|
146
222
|
- 사용자의 OAuth 액세스·갱신 토큰은 Git에 포함되지 않고 로컬 사용자 폴더에만 저장됩니다.
|
|
147
223
|
- 설치형 앱은 비밀을 유지할 수 없는 공개 클라이언트이므로 배포용 데스크톱 client ID와 client secret은 앱에 포함됩니다.
|
|
224
|
+
- 기존 자료 읽기 범위는 기본으로 요청하지 않고 `--read` 또는 `EDU_WORKSPACE_READ_ACCESS=1`로 켠 사람에게만 추가합니다.
|
|
148
225
|
- 검색·조회 도구에는 `readOnlyHint`를 표시합니다.
|
|
149
226
|
- Classroom 게시와 Drive 공유에는 `destructiveHint`를 표시합니다.
|
|
150
227
|
- Classroom 과제는 먼저 `DRAFT`로 만들고 별도 게시 도구에서 일회성 승인을 검증합니다.
|
|
151
228
|
- Drive 공유도 준비와 확정 단계를 분리합니다.
|
|
152
229
|
- 실제 학생 데이터가 아닌 별도 테스트 수업에서 먼저 검증하세요.
|
|
230
|
+
- Google API 할당량 오류에는 제한적 지수 백오프를 적용하고, 중복 생성 위험이 있는 모호한 오류는 자동 재시도하지 않습니다.
|
|
153
231
|
|
|
154
232
|
Google OAuth 브랜딩과 개인정보처리방침을 공개해야 하며, Google Workspace for Education 관리자가 제3자 앱을 허용해야 할 수 있습니다.
|
|
155
233
|
|
|
@@ -170,16 +248,21 @@ Google API 실계정 테스트에는 별도 테스트 계정을 사용하세요.
|
|
|
170
248
|
|
|
171
249
|
- [x] OAuth 로그인·토큰 저장·연결 해제
|
|
172
250
|
- [x] Drive 검색·폴더 생성·승인 공유
|
|
173
|
-
- [x] Docs·Sheets·Slides·Forms 생성
|
|
251
|
+
- [x] Docs·Sheets·Slides·Forms 생성 및 내용 읽기
|
|
252
|
+
- [x] 과정중심평가·Classroom 제출 대시보드 교육 템플릿
|
|
253
|
+
- [x] 개인정보 비노출 Sheets 구조·수식 오류 진단
|
|
174
254
|
- [x] Classroom 수업 조회·과제 초안·승인 게시
|
|
175
255
|
- [x] macOS·Windows·Linux 공통 npm 실행 구조
|
|
176
256
|
- [x] 단위·MCP 통합·stdio 스모크 테스트
|
|
177
257
|
- [x] 실계정 OAuth·Classroom 조회·Sheets 생성 E2E 테스트
|
|
178
|
-
- [
|
|
258
|
+
- [x] Google OAuth 브랜딩 검증·프로덕션 게시·비민감 범위 판정
|
|
259
|
+
- [x] PKCE·state·토큰 파일 보호·할당량 재시도 정책
|
|
260
|
+
- [x] Codex·Claude Code·Claude Desktop·Cursor 전역 자동 설치
|
|
179
261
|
- [ ] 호스팅형 Streamable HTTP MCP
|
|
180
262
|
- [ ] Calendar·Gmail·Tasks 확장
|
|
181
263
|
|
|
182
264
|
전체 제품 계획은 [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md)를 참고하세요.
|
|
265
|
+
정식판 출시 기준과 운영 정책은 [정식 배포 운영 기준](docs/PRODUCTION_READINESS.md), 버전별 변경 사항은 [CHANGELOG](CHANGELOG.md)를 참고하세요.
|
|
183
266
|
|
|
184
267
|
## 라이선스
|
|
185
268
|
|
|
@@ -5,8 +5,19 @@ export type AuthStatus = {
|
|
|
5
5
|
credentialsPath: string;
|
|
6
6
|
tokenPath: string;
|
|
7
7
|
grantedScopes: string[];
|
|
8
|
+
missingScopes?: string[];
|
|
9
|
+
unexpectedScopes?: string[];
|
|
10
|
+
tokenFileProtected?: boolean;
|
|
11
|
+
readAccessEnabled: boolean;
|
|
8
12
|
message: string;
|
|
9
13
|
};
|
|
14
|
+
export declare function validateOAuthCallback(callbackUrl: URL, expectedState: string): string;
|
|
10
15
|
export declare function getAuthStatus(): Promise<AuthStatus>;
|
|
11
16
|
export declare function login(): Promise<AuthStatus>;
|
|
12
17
|
export declare function getAuthorizedClient(): Promise<import("googleapis-common").OAuth2Client>;
|
|
18
|
+
export declare function disconnectGoogleAccount(options?: {
|
|
19
|
+
localOnly?: boolean;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
revoked: boolean;
|
|
22
|
+
tokenPath: string;
|
|
23
|
+
}>;
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { google } from "googleapis";
|
|
5
|
-
import {
|
|
7
|
+
import { CodeChallengeMethod } from "google-auth-library";
|
|
8
|
+
import { credentialsPath, googleScopes, readAccessEnabled, tokenPath } from "../config.js";
|
|
6
9
|
import { BUNDLED_GOOGLE_OAUTH_CLIENT } from "./bundled-oauth-client.js";
|
|
10
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
11
|
+
function createState() {
|
|
12
|
+
return randomBytes(32).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
export function validateOAuthCallback(callbackUrl, expectedState) {
|
|
15
|
+
const error = callbackUrl.searchParams.get("error");
|
|
16
|
+
if (error)
|
|
17
|
+
throw new Error(`Google OAuth 오류: ${error}`);
|
|
18
|
+
if (callbackUrl.searchParams.get("state") !== expectedState) {
|
|
19
|
+
throw new Error("OAuth state가 일치하지 않습니다. 로그인을 다시 시도하세요.");
|
|
20
|
+
}
|
|
21
|
+
const code = callbackUrl.searchParams.get("code");
|
|
22
|
+
if (!code)
|
|
23
|
+
throw new Error("OAuth 콜백에 인증 코드가 없습니다.");
|
|
24
|
+
return code;
|
|
25
|
+
}
|
|
26
|
+
async function openSystemBrowser(url) {
|
|
27
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
28
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
31
|
+
child.once("error", () => resolve(false));
|
|
32
|
+
child.once("spawn", () => {
|
|
33
|
+
child.unref();
|
|
34
|
+
resolve(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function protectTokenFile(filePath) {
|
|
39
|
+
if (process.platform === "win32")
|
|
40
|
+
return true;
|
|
41
|
+
try {
|
|
42
|
+
await chmod(filePath, 0o600);
|
|
43
|
+
return ((await stat(filePath)).mode & 0o077) === 0;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function writeTokenAtomically(filePath, value) {
|
|
50
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
51
|
+
const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
await writeFile(temporaryPath, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
54
|
+
await rename(temporaryPath, filePath);
|
|
55
|
+
if (!(await protectTokenFile(filePath)))
|
|
56
|
+
throw new Error("OAuth 토큰 파일 권한을 안전하게 설정하지 못했습니다.");
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await rm(temporaryPath, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
7
62
|
async function readJson(filePath) {
|
|
8
63
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
9
64
|
}
|
|
@@ -57,14 +112,29 @@ export async function getAuthStatus() {
|
|
|
57
112
|
if (oauthClientSource === "missing")
|
|
58
113
|
throw new Error("OAuth client missing");
|
|
59
114
|
const token = (await readJson(tokenFile));
|
|
115
|
+
const grantedScopes = token.scope?.split(" ").filter(Boolean) ?? [];
|
|
116
|
+
const expectedScopes = googleScopes();
|
|
117
|
+
const missingScopes = expectedScopes.filter((scope) => !grantedScopes.includes(scope));
|
|
118
|
+
const unexpectedScopes = grantedScopes.filter((scope) => !expectedScopes.includes(scope));
|
|
119
|
+
const tokenFileProtected = await protectTokenFile(tokenFile);
|
|
60
120
|
return {
|
|
61
121
|
authenticated: true,
|
|
62
122
|
oauthClientConfigured: true,
|
|
63
123
|
oauthClientSource,
|
|
64
124
|
credentialsPath: credentialFile,
|
|
65
125
|
tokenPath: tokenFile,
|
|
66
|
-
grantedScopes
|
|
67
|
-
|
|
126
|
+
grantedScopes,
|
|
127
|
+
missingScopes,
|
|
128
|
+
unexpectedScopes,
|
|
129
|
+
tokenFileProtected,
|
|
130
|
+
readAccessEnabled: readAccessEnabled(),
|
|
131
|
+
message: missingScopes.length
|
|
132
|
+
? "Google 토큰에 필요한 권한이 없습니다. disconnect 후 다시 로그인하세요."
|
|
133
|
+
: unexpectedScopes.length
|
|
134
|
+
? "Google 계정은 연결되어 있지만 이전의 추가 권한이 남아 있습니다. 최소 권한으로 다시 로그인하는 것을 권장합니다."
|
|
135
|
+
: readAccessEnabled()
|
|
136
|
+
? "Google 계정이 읽기 확장 권한으로 안전하게 연결되어 있습니다."
|
|
137
|
+
: "Google 계정이 기본 권한으로 안전하게 연결되어 있습니다."
|
|
68
138
|
};
|
|
69
139
|
}
|
|
70
140
|
catch {
|
|
@@ -75,6 +145,7 @@ export async function getAuthStatus() {
|
|
|
75
145
|
credentialsPath: credentialFile,
|
|
76
146
|
tokenPath: tokenFile,
|
|
77
147
|
grantedScopes: [],
|
|
148
|
+
readAccessEnabled: readAccessEnabled(),
|
|
78
149
|
message: oauthClientSource === "missing"
|
|
79
150
|
? "공용 Google OAuth 앱이 아직 구성되지 않았습니다. 개발용 credentials.json 또는 환경 변수가 필요합니다."
|
|
80
151
|
: "Google 계정이 연결되지 않았습니다. edu-workspace-mcp login을 실행하세요."
|
|
@@ -82,24 +153,32 @@ export async function getAuthStatus() {
|
|
|
82
153
|
}
|
|
83
154
|
}
|
|
84
155
|
export async function login() {
|
|
85
|
-
const credentialFile = credentialsPath();
|
|
86
156
|
const tokenFile = tokenPath();
|
|
87
157
|
const { config } = await loadClientConfig();
|
|
158
|
+
const expectedState = createState();
|
|
88
159
|
const callbackServer = createServer();
|
|
89
160
|
const authorizationCode = new Promise((resolve, reject) => {
|
|
90
|
-
|
|
161
|
+
const timeout = setTimeout(() => reject(new Error("Google 로그인 대기 시간이 만료되었습니다. 다시 시도하세요.")), LOGIN_TIMEOUT_MS);
|
|
162
|
+
callbackServer.on("request", (request, response) => {
|
|
91
163
|
const callbackUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
164
|
+
if (callbackUrl.pathname !== "/oauth2callback") {
|
|
165
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
166
|
+
response.end("Not found");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const code = validateOAuthCallback(callbackUrl, expectedState);
|
|
171
|
+
clearTimeout(timeout);
|
|
172
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
173
|
+
response.end("로그인이 완료되었습니다. 이 창을 닫고 터미널로 돌아가세요.");
|
|
174
|
+
resolve(code);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
clearTimeout(timeout);
|
|
95
178
|
response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
96
179
|
response.end("Google 로그인을 완료하지 못했습니다. 터미널에서 다시 시도하세요.");
|
|
97
|
-
reject(
|
|
98
|
-
return;
|
|
180
|
+
reject(error);
|
|
99
181
|
}
|
|
100
|
-
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
101
|
-
response.end("로그인이 완료되었습니다. 이 창을 닫고 터미널로 돌아가세요.");
|
|
102
|
-
resolve(code);
|
|
103
182
|
});
|
|
104
183
|
});
|
|
105
184
|
await new Promise((resolve, reject) => {
|
|
@@ -113,12 +192,17 @@ export async function login() {
|
|
|
113
192
|
}
|
|
114
193
|
const redirectUri = `http://127.0.0.1:${address.port}/oauth2callback`;
|
|
115
194
|
const client = createOAuthClient(config, redirectUri);
|
|
195
|
+
const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();
|
|
116
196
|
const authUrl = client.generateAuthUrl({
|
|
117
197
|
access_type: "offline",
|
|
118
198
|
prompt: "consent",
|
|
119
|
-
scope:
|
|
199
|
+
scope: googleScopes(),
|
|
200
|
+
state: expectedState,
|
|
201
|
+
code_challenge: codeChallenge,
|
|
202
|
+
code_challenge_method: CodeChallengeMethod.S256
|
|
120
203
|
});
|
|
121
|
-
|
|
204
|
+
const browserOpened = await openSystemBrowser(authUrl);
|
|
205
|
+
console.log(browserOpened ? "기본 브라우저에서 Google 로그인을 완료하세요.\n" : "브라우저에서 아래 URL을 열어 Google 로그인을 완료하세요:\n");
|
|
122
206
|
console.log(authUrl);
|
|
123
207
|
let code;
|
|
124
208
|
try {
|
|
@@ -127,12 +211,11 @@ export async function login() {
|
|
|
127
211
|
finally {
|
|
128
212
|
callbackServer.close();
|
|
129
213
|
}
|
|
130
|
-
const { tokens } = await client.getToken(code);
|
|
214
|
+
const { tokens } = await client.getToken({ code, codeVerifier, redirect_uri: redirectUri });
|
|
131
215
|
if (!tokens.refresh_token) {
|
|
132
216
|
throw new Error("갱신 토큰을 받지 못했습니다. Google 계정에서 기존 앱 권한을 해제한 뒤 다시 로그인하세요.");
|
|
133
217
|
}
|
|
134
|
-
await
|
|
135
|
-
await writeFile(tokenFile, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
218
|
+
await writeTokenAtomically(tokenFile, tokens);
|
|
136
219
|
return getAuthStatus();
|
|
137
220
|
}
|
|
138
221
|
export async function getAuthorizedClient() {
|
|
@@ -142,4 +225,20 @@ export async function getAuthorizedClient() {
|
|
|
142
225
|
client.setCredentials((await readJson(tokenFile)));
|
|
143
226
|
return client;
|
|
144
227
|
}
|
|
228
|
+
export async function disconnectGoogleAccount(options = {}) {
|
|
229
|
+
const tokenFile = tokenPath();
|
|
230
|
+
let revoked = false;
|
|
231
|
+
if (!options.localOnly) {
|
|
232
|
+
const token = (await readJson(tokenFile));
|
|
233
|
+
const credential = token.refresh_token ?? token.access_token;
|
|
234
|
+
if (!credential)
|
|
235
|
+
throw new Error("철회할 Google OAuth 토큰을 찾지 못했습니다. --local-only로 로컬 파일만 삭제할 수 있습니다.");
|
|
236
|
+
const { config } = await loadClientConfig();
|
|
237
|
+
const client = createOAuthClient(config);
|
|
238
|
+
await client.revokeToken(credential);
|
|
239
|
+
revoked = true;
|
|
240
|
+
}
|
|
241
|
+
await rm(tokenFile, { force: true });
|
|
242
|
+
return { revoked, tokenPath: tokenFile };
|
|
243
|
+
}
|
|
145
244
|
//# sourceMappingURL=google-auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth.js","sourceRoot":"","sources":["../../src/auth/google-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"google-auth.js","sourceRoot":"","sources":["../../src/auth/google-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdir
|
|
3
|
-
import { APP_DIRECTORY, credentialsPath
|
|
4
|
-
import { getAuthStatus, getAuthorizedClient, login } from "./auth/google-auth.js";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { APP_DIRECTORY, credentialsPath } from "./config.js";
|
|
4
|
+
import { disconnectGoogleAccount, getAuthStatus, getAuthorizedClient, login } from "./auth/google-auth.js";
|
|
5
|
+
import { installClient } from "./install/configure-client.js";
|
|
6
|
+
import { APP_VERSION, PACKAGE_SPEC } from "./version.js";
|
|
5
7
|
function printHelp() {
|
|
6
8
|
console.log(`
|
|
7
9
|
edu-workspace-mcp
|
|
8
10
|
|
|
9
11
|
명령어:
|
|
10
12
|
setup 초기 폴더와 MCP 설정 예시를 보여줍니다.
|
|
13
|
+
install AI 클라이언트 전체 프로젝트 설정에 MCP를 등록합니다.
|
|
11
14
|
login Google OAuth 로그인 후 토큰을 안전한 로컬 경로에 저장합니다.
|
|
12
15
|
doctor OAuth 자격 증명·토큰 상태를 확인합니다.
|
|
13
|
-
disconnect
|
|
16
|
+
disconnect Google 권한을 철회하고 로컬 토큰을 삭제합니다.
|
|
14
17
|
serve stdio MCP 서버를 시작합니다. 명령이 없을 때의 기본 동작입니다.
|
|
18
|
+
version 설치된 버전을 표시합니다.
|
|
19
|
+
|
|
20
|
+
설치 예시:
|
|
21
|
+
npx -y ${PACKAGE_SPEC} install codex
|
|
22
|
+
npx -y ${PACKAGE_SPEC} install claude --read
|
|
23
|
+
npx -y ${PACKAGE_SPEC} install claude-desktop --read
|
|
24
|
+
npx -y ${PACKAGE_SPEC} install cursor
|
|
25
|
+
|
|
26
|
+
읽기 확장:
|
|
27
|
+
install과 login에 --read를 붙이면 기존 Docs·Sheets·Slides·Forms와
|
|
28
|
+
Classroom 학생 명단을 읽을 수 있는 읽기 전용 범위를 요청합니다.
|
|
15
29
|
`);
|
|
16
30
|
}
|
|
17
31
|
async function main() {
|
|
@@ -25,33 +39,60 @@ async function main() {
|
|
|
25
39
|
const status = await getAuthStatus();
|
|
26
40
|
console.log(`\n설정 폴더: ${APP_DIRECTORY}`);
|
|
27
41
|
console.log(`OAuth 클라이언트: ${status.oauthClientConfigured ? `${status.oauthClientSource} 설정 사용` : "아직 구성되지 않음"}`);
|
|
28
|
-
console.log(`\
|
|
29
|
-
console.log(`\
|
|
42
|
+
console.log(`\n권장 자동 설치:\n npx -y ${PACKAGE_SPEC} install codex --read\n npx -y ${PACKAGE_SPEC} install claude --read\n npx -y ${PACKAGE_SPEC} install claude-desktop --read\n npx -y ${PACKAGE_SPEC} install cursor --read`);
|
|
43
|
+
console.log(`\nCodex/ChatGPT Desktop 예시:\n[mcp_servers.edu-workspace]\ncommand = "npx"\nargs = ["-y", "${PACKAGE_SPEC}"]`);
|
|
44
|
+
console.log(`\nClaude Desktop/Cursor 예시:\n{\n "mcpServers": {\n "edu-workspace": {\n "command": "npx",\n "args": ["-y", "${PACKAGE_SPEC}"]\n }\n }\n}`);
|
|
30
45
|
console.log(status.oauthClientConfigured
|
|
31
|
-
?
|
|
46
|
+
? `\n\`npx -y ${PACKAGE_SPEC} login\`으로 Google 계정을 연결하세요.`
|
|
32
47
|
: `\n공개 배포 전에는 배포자가 공용 OAuth 앱을 설정해야 합니다. 개발 중에는 ${credentialsPath()}에 credentials.json을 둘 수 있습니다.`);
|
|
33
48
|
return;
|
|
34
49
|
}
|
|
50
|
+
if (command === "install") {
|
|
51
|
+
const client = process.argv[3];
|
|
52
|
+
if (!client || !["codex", "claude", "claude-desktop", "cursor"].includes(client)) {
|
|
53
|
+
throw new Error("설치 대상을 지정하세요: codex, claude, claude-desktop, cursor");
|
|
54
|
+
}
|
|
55
|
+
const readAccess = process.argv.includes("--read");
|
|
56
|
+
console.log(await installClient(client, { readAccess }));
|
|
57
|
+
console.log(`다음 단계: npx -y ${PACKAGE_SPEC} login${readAccess ? " --read" : ""}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
35
60
|
if (command === "login") {
|
|
61
|
+
if (process.argv.includes("--read"))
|
|
62
|
+
process.env.EDU_WORKSPACE_READ_ACCESS = "1";
|
|
36
63
|
const status = await login();
|
|
37
64
|
console.log(status.message);
|
|
38
65
|
console.log(`토큰 저장 위치: ${status.tokenPath}`);
|
|
39
66
|
return;
|
|
40
67
|
}
|
|
41
68
|
if (command === "doctor") {
|
|
69
|
+
if (process.argv.includes("--read"))
|
|
70
|
+
process.env.EDU_WORKSPACE_READ_ACCESS = "1";
|
|
42
71
|
const status = await getAuthStatus();
|
|
43
|
-
console.log(JSON.stringify(status, null, 2));
|
|
72
|
+
console.log(JSON.stringify({ version: APP_VERSION, node: process.version, ...status }, null, 2));
|
|
73
|
+
if (Number(process.versions.node.split(".")[0]) < 20)
|
|
74
|
+
throw new Error("Node.js 20 이상이 필요합니다.");
|
|
44
75
|
if (status.authenticated) {
|
|
45
76
|
const client = await getAuthorizedClient();
|
|
46
77
|
await client.getAccessToken();
|
|
47
78
|
console.log("Google OAuth 토큰 갱신: 정상");
|
|
79
|
+
if (status.missingScopes?.length)
|
|
80
|
+
throw new Error("필수 OAuth 범위가 누락되었습니다. disconnect 후 다시 로그인하세요.");
|
|
81
|
+
if (status.unexpectedScopes?.length)
|
|
82
|
+
console.warn("경고: 이전의 추가 OAuth 권한이 남아 있습니다. 최소 권한 재로그인을 권장합니다.");
|
|
83
|
+
if (status.tokenFileProtected === false)
|
|
84
|
+
throw new Error("OAuth 토큰 파일 권한이 안전하지 않습니다.");
|
|
48
85
|
}
|
|
49
86
|
return;
|
|
50
87
|
}
|
|
88
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
89
|
+
console.log(APP_VERSION);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
51
92
|
if (command === "disconnect") {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log("Google
|
|
93
|
+
const localOnly = process.argv.includes("--local-only");
|
|
94
|
+
const result = await disconnectGoogleAccount({ localOnly });
|
|
95
|
+
console.log(`${result.revoked ? "Google OAuth 권한을 철회하고 " : ""}로컬 토큰을 제거했습니다: ${result.tokenPath}`);
|
|
55
96
|
return;
|
|
56
97
|
}
|
|
57
98
|
printHelp();
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|