edu-workspace-mcp 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/approvals/service.d.ts +9 -0
  4. package/dist/approvals/service.js +27 -0
  5. package/dist/approvals/service.js.map +1 -0
  6. package/dist/auth/bundled-oauth-client.d.ts +11 -0
  7. package/dist/auth/bundled-oauth-client.js +12 -0
  8. package/dist/auth/bundled-oauth-client.js.map +1 -0
  9. package/dist/auth/google-auth.d.ts +12 -0
  10. package/dist/auth/google-auth.js +145 -0
  11. package/dist/auth/google-auth.js.map +1 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.js +63 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +7 -0
  16. package/dist/config.js +18 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/google/classroom.d.ts +39 -0
  19. package/dist/google/classroom.js +77 -0
  20. package/dist/google/classroom.js.map +1 -0
  21. package/dist/google/docs.d.ts +9 -0
  22. package/dist/google/docs.js +30 -0
  23. package/dist/google/docs.js.map +1 -0
  24. package/dist/google/drive.d.ts +4 -0
  25. package/dist/google/drive.js +61 -0
  26. package/dist/google/drive.js.map +1 -0
  27. package/dist/google/forms.d.ts +14 -0
  28. package/dist/google/forms.js +57 -0
  29. package/dist/google/forms.js.map +1 -0
  30. package/dist/google/sheets.d.ts +9 -0
  31. package/dist/google/sheets.js +33 -0
  32. package/dist/google/sheets.js.map +1 -0
  33. package/dist/google/slides.d.ts +9 -0
  34. package/dist/google/slides.js +63 -0
  35. package/dist/google/slides.js.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +6 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/server.d.ts +22 -0
  40. package/dist/server.js +211 -0
  41. package/dist/server.js.map +1 -0
  42. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 edu-workspace-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # πŸŽ“ edu-workspace-mcp
2
+
3
+ μžμ—°μ–΄λ‘œ Google Docs, Sheets, Slides, Forms, Drive와 Classroom을 μ‚¬μš©ν•˜λŠ” ꡐ윑 νŠΉν™” Model Context Protocol(MCP) μ„œλ²„μž…λ‹ˆλ‹€. Apps Script 없이 Google 곡식 APIλ₯Ό 직접 ν˜ΈμΆœν•©λ‹ˆλ‹€.
4
+
5
+ [곡식 ν™ˆνŽ˜μ΄μ§€](https://edu.jeld.kr/) Β· [κ°œμΈμ •λ³΄μ²˜λ¦¬λ°©μΉ¨](https://edu.jeld.kr/privacy.html) Β· [μ΄μš©μ•½κ΄€](https://edu.jeld.kr/terms.html)
6
+
7
+ > ν˜„μž¬ 버전은 둜컬 stdio MCP λ² νƒ€μž…λ‹ˆλ‹€. 곡용 Google λ°μŠ€ν¬ν†± OAuth ν΄λΌμ΄μ–ΈνŠΈκ°€ ν¬ν•¨λ˜μ–΄ μžˆμ–΄ μ΅œμ’… μ‚¬μš©μžκ°€ Google Cloud ν”„λ‘œμ νŠΈλ₯Ό λ§Œλ“€ ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€. 앱은 μ œν’ˆ 전체 λ²”μœ„ λŒ€μ‹  앱이 μ‚¬μš©ν•˜λŠ” νŒŒμΌμ— ν•œμ •λœ `drive.file` κΆŒν•œμ„ μš”μ²­ν•©λ‹ˆλ‹€.
8
+
9
+ ## ν•  수 μžˆλŠ” 일
10
+
11
+ - μˆ˜μ—… 자료용 Google Docs λ¬Έμ„œ 생성
12
+ - 학생 λͺ…λ‹¨Β·λ£¨λΈŒλ¦­Β·μ„±μ ν‘œ ν˜•νƒœμ˜ Google Sheets 생성
13
+ - μ°¨μ‹œλ³„ Google Slides ν”„λ ˆμ  ν…Œμ΄μ…˜ 생성
14
+ - 정닡·배점이 ν¬ν•¨λœ Google Forms ν€΄μ¦ˆ 생성
15
+ - Google Drive 파일 검색, 폴더 생성, 승인 ν›„ 곡유
16
+ - ꡐ사가 λ‹΄λ‹Ήν•˜λŠ” Google Classroom μˆ˜μ—… 쑰회
17
+ - Classroom 과제 μ΄ˆμ•ˆ 생성, μ‚¬μš©μž 승인 ν›„ κ²Œμ‹œ
18
+
19
+ μ˜ˆμ‹œ μš”μ²­:
20
+
21
+ > β€œ`μ†Œν™”μ™€ μˆœν™˜` λ‹¨μ›μ˜ 5λ¬Έν•­ ν€΄μ¦ˆμ™€ ν•™μŠ΅μ§€, 6μž₯짜리 μˆ˜μ—… μŠ¬λΌμ΄λ“œλ₯Ό λ§Œλ“€κ³  `2ν•™λ…„ κ³Όν•™` ν΄λž˜μŠ€λ£Έμ— κΈˆμš”μΌ μ˜€ν›„ 6μ‹œ 마감 과제 μ΄ˆμ•ˆμœΌλ‘œ λ“±λ‘ν•΄μ€˜.”
22
+
23
+ ## μ‹€μ œ ꡐ윑 ν™œμš© 데λͺ¨
24
+
25
+ ν•œ 번의 μžμ—°μ–΄ μš”μ²­μœΌλ‘œ μ—¬λŸ¬ Google ꡐ윑 도ꡬλ₯Ό μ—°κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
26
+
27
+ ```text
28
+ ꡐ사 μš”μ²­
29
+ β†’ Drive μˆ˜μ—… 폴더 생성
30
+ β†’ Docs κ΅μ‚¬μš© μˆ˜μ—…μ•ˆΒ·ν•™μƒμš© ν•™μŠ΅μ§€ 생성
31
+ β†’ Slides μˆ˜μ—… 자료 생성
32
+ β†’ Forms 정닡·배점 포함 ν€΄μ¦ˆ 생성
33
+ β†’ Classroom 과제 μ΄ˆμ•ˆ 생성
34
+ β†’ ꡐ사가 λŒ€μƒΒ·λ§ˆκ°Β·μ²¨λΆ€λ₯Ό ν™•μΈν•œ λ’€ κ²Œμ‹œ
35
+ ```
36
+
37
+ λŒ€ν‘œ 데λͺ¨ ν”„λ‘¬ν”„νŠΈ:
38
+
39
+ > 5ν•™λ…„ κ³Όν•™ β€˜μ†Œν™”μ™€ μˆœν™˜β€™ μˆ˜μ—… νŒ¨ν‚€μ§€λ₯Ό λ§Œλ“€μ–΄μ€˜. Drive에 μ „μš© 폴더λ₯Ό λ§Œλ“€κ³ , κ΅μ‚¬μš© μˆ˜μ—…μ•ˆκ³Ό ν•™μƒμš© ν•™μŠ΅μ§€λŠ” Docs둜, 6μž₯ μˆ˜μ—… μžλ£ŒλŠ” Slides둜, 5λ¬Έν•­ ν˜•μ„±ν‰κ°€λŠ” Forms ν€΄μ¦ˆλ‘œ λ§Œλ“€μ–΄μ€˜. β€˜5ν•™λ…„ 3반 과학’ Classroomμ—λŠ” 과제 μ΄ˆμ•ˆκΉŒμ§€λ§Œ λ§Œλ“€κ³  κ²Œμ‹œ 전에 λ‚˜μ—κ²Œ ν™•μΈλ°›μ•„μ€˜.
40
+
41
+ | ꡐ윑 업무 | λ§Œλ“€μ–΄μ§€λŠ” κ²°κ³Ό | μ‚¬μš©ν•˜λŠ” 도ꡬ |
42
+ | --- | --- | --- |
43
+ | ν•œ μ°¨μ‹œ μˆ˜μ—… νŒ¨ν‚€μ§€ | μˆ˜μ—…μ•ˆ, ν•™μŠ΅μ§€, μŠ¬λΌμ΄λ“œ, ν€΄μ¦ˆ, Classroom μ΄ˆμ•ˆ | Drive + Docs + Slides + Forms + Classroom |
44
+ | 과정쀑심평가 관리 | ν‰κ°€μž…λ ₯Β·ν•™μƒλ³„ν˜„ν™©Β·ν•­λͺ©λ³„ν˜„ν™© μ‹œνŠΈ | Sheets |
45
+ | μˆ˜ν–‰ν‰κ°€ 섀계 | 4μˆ˜μ€€ 루브릭 λ¬Έμ„œμ™€ 학생 μžκΈ°ν‰κ°€μ§€ | Docs + Forms |
46
+ | ν˜•μ„±ν‰κ°€ 배포 | 정닡·배점이 μžˆλŠ” ν€΄μ¦ˆμ™€ 과제 μ΄ˆμ•ˆ | Forms + Classroom |
47
+ | ν•™κΈ° 자료 정리 | 검색 결과와 ꡐ과·단원별 Drive 폴더 | Drive |
48
+ | λ™λ£Œ ꡐ사 ν˜‘μ—… | 확인 절차λ₯Ό 거친 λŒ“κΈ€Β·μ½κΈ°Β·νŽΈμ§‘ κΆŒν•œ | Drive 곡유 |
49
+
50
+ - [ꡐ윑자λ₯Ό μœ„ν•œ μ‹€μ œ ν™œμš© μ˜ˆμ‹œμ™€ 볡사 κ°€λŠ₯ν•œ ν”„λ‘¬ν”„νŠΈ](docs/EDUCATOR_USE_CASES.md)
51
+ - [예제 λͺ¨μŒκ³Ό μΆ”μ²œ 첫 데λͺ¨](examples/README.md)
52
+ - [μ‹€μ œλ‘œ μ‹€ν–‰λ˜λŠ” μˆ˜μ—… νŒ¨ν‚€μ§€ 톡합 ν…ŒμŠ€νŠΈ](src/tests/education-demo.test.ts)
53
+
54
+ > v0.1은 μƒˆ 파일 생성과 Classroom μ΄ˆμ•ˆΒ·κ²Œμ‹œ 흐름에 μ§‘μ€‘ν•©λ‹ˆλ‹€. κΈ°μ‘΄ λ¬Έμ„œμ˜ λΆ€λΆ„ νŽΈμ§‘, Sheets μ°¨νŠΈΒ·λ“œλ‘­λ‹€μš΄Β·μ‘°κ±΄λΆ€ μ„œμ‹, Forms 응닡과 학생 제좜물 뢄석은 아직 μ§€μ›ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μžμ„Έν•œ 지원 λ²”μœ„λŠ” [ν™œμš© μ˜ˆμ‹œ λ¬Έμ„œ](docs/EDUCATOR_USE_CASES.md#ν˜„μž¬-λ²„μ „μ—μ„œ-κ°€λŠ₯ν•œ-λ²”μœ„)λ₯Ό ν™•μΈν•˜μ„Έμš”.
55
+
56
+ ## MCP 도ꡬ
57
+
58
+ | 도ꡬ | λ™μž‘ | 성격 |
59
+ | --- | --- | --- |
60
+ | `workspace_get_auth_status` | Google μ—°κ²° μƒνƒœμ™€ λ²”μœ„ 확인 | 읽기 |
61
+ | `classroom_list_courses` | ꡐ사가 λ‹΄λ‹Ήν•˜λŠ” ν™œμ„± μˆ˜μ—… 쑰회 | 읽기 |
62
+ | `drive_search_files` | μ΄λ¦„Β·μœ ν˜•Β·ν΄λ”λ‘œ Drive 검색 | 읽기 |
63
+ | `drive_create_folder` | Drive 폴더 생성 | 생성 |
64
+ | `docs_create_document` | Docs λ¬Έμ„œ 생성 | 생성 |
65
+ | `sheets_create_workbook` | μ—¬λŸ¬ νƒ­κ³Ό 초기 데이터가 μžˆλŠ” Sheets 생성 | 생성 |
66
+ | `slides_create_presentation` | 제λͺ©Β·λ³Έλ¬Έ μŠ¬λΌμ΄λ“œ 생성 | 생성 |
67
+ | `forms_create_quiz` | 문항·정닡·배점이 μžˆλŠ” ν€΄μ¦ˆ 생성 | 생성 |
68
+ | `classroom_create_assignment_draft` | ν•™μƒμ—κ²Œ 보이지 μ•ŠλŠ” 과제 μ΄ˆμ•ˆ 생성 | 생성 |
69
+ | `classroom_publish_assignment` | 승인된 과제 μ΄ˆμ•ˆμ„ ν•™μƒμ—κ²Œ κ²Œμ‹œ | μ™ΈλΆ€ λ³€κ²½ |
70
+ | `drive_prepare_share` | 곡유 λŒ€μƒΒ·κΆŒν•œ 승인 μ€€λΉ„ | 승인 μ€€λΉ„ |
71
+ | `drive_share_file` | 승인된 Drive κΆŒν•œ λ³€κ²½ | μ™ΈλΆ€ λ³€κ²½ |
72
+
73
+ κ²Œμ‹œμ™€ 곡유 λ„κ΅¬μ—λŠ” MCP μ•ˆμ „ 메타데이터가 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. 승인 IDλŠ” 15λΆ„ λ™μ•ˆ μœ νš¨ν•˜κ³  ν•œ 번만 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
74
+
75
+ ## μš”κ΅¬ 사항
76
+
77
+ - Node.js 20 이상
78
+ - MCPλ₯Ό μ§€μ›ν•˜λŠ” λ°μŠ€ν¬ν†± ν΄λΌμ΄μ–ΈνŠΈ
79
+ - Google 계정
80
+
81
+ ## λΉ λ₯Έ μ„€μΉ˜ β€” μ΅œμ’… μ‚¬μš©μž
82
+
83
+ 정식 npm 배포 ν›„ macOS, Windows, Linuxμ—μ„œ λ‹€μŒ 두 λ‹¨κ³„λ§Œ μ§„ν–‰ν•©λ‹ˆλ‹€. Google Cloud Console 섀정은 ν•„μš”ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
84
+
85
+ ```bash
86
+ npx -y edu-workspace-mcp setup
87
+ npx -y edu-workspace-mcp login
88
+ ```
89
+
90
+ λΈŒλΌμš°μ €μ—μ„œ Google 둜그인과 κΆŒν•œ μŠΉμΈμ„ 마치고 MCP ν΄λΌμ΄μ–ΈνŠΈμ— μ—°κ²°ν•˜λ©΄ λ©λ‹ˆλ‹€. 학ꡐ κ³„μ •μ—μ„œ κΈ°κ΄€ κ΄€λ¦¬μžκ°€ 제3자 앱을 μ°¨λ‹¨ν•œ κ²½μš°μ—λŠ” κ΄€λ¦¬μžμ—κ²Œ μ•± ν—ˆμš©μ„ μš”μ²­ν•΄μ•Ό ν•©λ‹ˆλ‹€.
91
+
92
+ ```bash
93
+ npx -y edu-workspace-mcp doctor
94
+ ```
95
+
96
+ Google 토큰은 `~/.edu-workspace-mcp/token.json`에 μ‚¬μš©μž μ „μš© κΆŒν•œμœΌλ‘œ μ €μž₯λ©λ‹ˆλ‹€. 연결을 ν•΄μ œν•˜λ €λ©΄ λ‹€μŒμ„ μ‹€ν–‰ν•©λ‹ˆλ‹€.
97
+
98
+ ```bash
99
+ npx -y edu-workspace-mcp disconnect
100
+ ```
101
+
102
+ ## MCP ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²°
103
+
104
+ Claude Desktopκ³Ό Cursor 계열 JSON μ„€μ •:
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "edu-workspace": {
110
+ "command": "npx",
111
+ "args": ["-y", "edu-workspace-mcp"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ CodexΒ·ChatGPT Desktop 계열 TOML μ„€μ •:
118
+
119
+ ```toml
120
+ [mcp_servers.edu_workspace]
121
+ command = "npx"
122
+ args = ["-y", "edu-workspace-mcp"]
123
+ default_tools_approval_mode = "writes"
124
+ ```
125
+
126
+ ChatGPT 웹은 μ‚¬μš©μž μ»΄ν“¨ν„°μ˜ stdio ν”„λ‘œμ„ΈμŠ€λ₯Ό μ‹€ν–‰ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μ›Ή 지원은 ν–₯ν›„ ν˜ΈμŠ€νŒ…ν˜• Streamable HTTP μ„œλ²„λ‘œ μ œκ³΅ν•  κ³„νšμž…λ‹ˆλ‹€.
127
+
128
+ ## μ†ŒμŠ€μ—μ„œ μ‹€ν–‰
129
+
130
+ 곡개 μ €μž₯μ†Œμ—λŠ” μ„€μΉ˜ν˜• μ•±μš© 곡용 OAuth client IDκ°€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ΅œμ’… μ‚¬μš©μžλŠ” 별도 Google Cloud ν”„λ‘œμ νŠΈλ‚˜ `credentials.json` 없이 `login`만 μ‹€ν–‰ν•©λ‹ˆλ‹€.
131
+
132
+ ```bash
133
+ npm install
134
+ npm run check
135
+ npm test
136
+ npm run test:mcp
137
+ node dist/cli.js setup
138
+ node dist/cli.js login
139
+ ```
140
+
141
+ 개발 쀑 MCP μ„€μ •μ—μ„œλŠ” `command`에 `node`, `args`에 ν”„λ‘œμ νŠΈμ˜ μ ˆλŒ€ 경둜인 `dist/cli.js`λ₯Ό μ§€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
142
+
143
+ ## λ³΄μ•ˆ λͺ¨λΈ
144
+
145
+ - ν•„μš”ν•œ Google OAuth λ²”μœ„λ§Œ μš”μ²­ν•©λ‹ˆλ‹€.
146
+ - μ‚¬μš©μžμ˜ OAuth μ•‘μ„ΈμŠ€Β·κ°±μ‹  토큰은 Git에 ν¬ν•¨λ˜μ§€ μ•Šκ³  둜컬 μ‚¬μš©μž ν΄λ”μ—λ§Œ μ €μž₯λ©λ‹ˆλ‹€.
147
+ - μ„€μΉ˜ν˜• 앱은 비밀을 μœ μ§€ν•  수 μ—†λŠ” 곡개 ν΄λΌμ΄μ–ΈνŠΈμ΄λ―€λ‘œ 배포용 λ°μŠ€ν¬ν†± client ID와 client secret은 앱에 ν¬ν•¨λ©λ‹ˆλ‹€.
148
+ - κ²€μƒ‰Β·μ‘°νšŒ λ„κ΅¬μ—λŠ” `readOnlyHint`λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€.
149
+ - Classroom κ²Œμ‹œμ™€ Drive κ³΅μœ μ—λŠ” `destructiveHint`λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€.
150
+ - Classroom κ³Όμ œλŠ” λ¨Όμ € `DRAFT`둜 λ§Œλ“€κ³  별도 κ²Œμ‹œ λ„κ΅¬μ—μ„œ μΌνšŒμ„± μŠΉμΈμ„ κ²€μ¦ν•©λ‹ˆλ‹€.
151
+ - Drive κ³΅μœ λ„ 쀀비와 ν™•μ • 단계λ₯Ό λΆ„λ¦¬ν•©λ‹ˆλ‹€.
152
+ - μ‹€μ œ 학생 데이터가 μ•„λ‹Œ 별도 ν…ŒμŠ€νŠΈ μˆ˜μ—…μ—μ„œ λ¨Όμ € κ²€μ¦ν•˜μ„Έμš”.
153
+
154
+ Google OAuth λΈŒλžœλ”©κ³Ό κ°œμΈμ •λ³΄μ²˜λ¦¬λ°©μΉ¨μ„ κ³΅κ°œν•΄μ•Ό ν•˜λ©°, Google Workspace for Education κ΄€λ¦¬μžκ°€ 제3자 앱을 ν—ˆμš©ν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
155
+
156
+ μš”μ²­ λ²”μœ„μ˜ 상세 μ„€λͺ…은 [OAuth λ²”μœ„ λ¬Έμ„œ](docs/OAUTH_SCOPES.md), 곡개 데이터 처리 방침은 [κ°œμΈμ •λ³΄μ²˜λ¦¬λ°©μΉ¨](https://edu.jeld.kr/privacy.html)을 μ°Έκ³ ν•˜μ„Έμš”. 곡개 배포λ₯Ό μœ„ν•œ λ²”μœ„λ³„ μ‚¬μš© μ‚¬μœ μ™€ 데λͺ¨ μ˜μƒ μ‹œλ‚˜λ¦¬μ˜€λŠ” [OAuth κ²€μˆ˜ 제좜 κ°€μ΄λ“œ](docs/OAUTH_VERIFICATION_GUIDE.md)에 μ •λ¦¬λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.
157
+
158
+ ## ν…ŒμŠ€νŠΈ
159
+
160
+ ```bash
161
+ npm run check # TypeScript 정적 검사
162
+ npm test # λ‹¨μœ„Β·MCP 인메λͺ¨λ¦¬ 톡합 ν…ŒμŠ€νŠΈ
163
+ npm run test:mcp # μ‹€μ œ stdio ν”„λ‘œμ„ΈμŠ€ μ—°κ²° ν…ŒμŠ€νŠΈ
164
+ npm audit --omit=dev
165
+ ```
166
+
167
+ Google API 싀계정 ν…ŒμŠ€νŠΈμ—λŠ” 별도 ν…ŒμŠ€νŠΈ 계정을 μ‚¬μš©ν•˜μ„Έμš”. CIμ—μ„œλŠ” μ™ΈλΆ€ 계정을 μ‚¬μš©ν•˜μ§€ μ•Šκ³  Google API μ–΄λŒ‘ν„°λ₯Ό μ£Όμž…ν•œ 계약 ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
168
+
169
+ ## 개발 ν˜„ν™©
170
+
171
+ - [x] OAuth λ‘œκ·ΈμΈΒ·ν† ν° μ €μž₯Β·μ—°κ²° ν•΄μ œ
172
+ - [x] Drive 검색·폴더 μƒμ„±Β·μŠΉμΈ 곡유
173
+ - [x] DocsΒ·SheetsΒ·SlidesΒ·Forms 생성
174
+ - [x] Classroom μˆ˜μ—… 쑰회·과제 μ΄ˆμ•ˆΒ·μŠΉμΈ κ²Œμ‹œ
175
+ - [x] macOSΒ·WindowsΒ·Linux 곡톡 npm μ‹€ν–‰ ꡬ쑰
176
+ - [x] λ‹¨μœ„Β·MCP 톡합·stdio 슀λͺ¨ν¬ ν…ŒμŠ€νŠΈ
177
+ - [x] 싀계정 OAuthΒ·Classroom 쑰회·Sheets 생성 E2E ν…ŒμŠ€νŠΈ
178
+ - [ ] Google OAuth 곡개 μ•± 검증
179
+ - [ ] ν˜ΈμŠ€νŒ…ν˜• Streamable HTTP MCP
180
+ - [ ] CalendarΒ·GmailΒ·Tasks ν™•μž₯
181
+
182
+ 전체 μ œν’ˆ κ³„νšμ€ [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md)λ₯Ό μ°Έκ³ ν•˜μ„Έμš”.
183
+
184
+ ## λΌμ΄μ„ μŠ€
185
+
186
+ [MIT](LICENSE)
@@ -0,0 +1,9 @@
1
+ export type ApprovalRecord = {
2
+ id: string;
3
+ action: "classroom.publish" | "drive.share";
4
+ summary: Record<string, unknown>;
5
+ expiresAt: string;
6
+ };
7
+ export declare function createApproval(action: ApprovalRecord["action"], summary: Record<string, unknown>): ApprovalRecord;
8
+ export declare function consumeApproval(id: string, action: ApprovalRecord["action"]): ApprovalRecord;
9
+ export declare function resetApprovalsForTests(): void;
@@ -0,0 +1,27 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const approvals = new Map();
3
+ export function createApproval(action, summary) {
4
+ const record = {
5
+ id: randomUUID(),
6
+ action,
7
+ summary,
8
+ expiresAt: new Date(Date.now() + 15 * 60_000).toISOString()
9
+ };
10
+ approvals.set(record.id, record);
11
+ return record;
12
+ }
13
+ export function consumeApproval(id, action) {
14
+ const record = approvals.get(id);
15
+ if (!record || record.action !== action)
16
+ throw new Error("승인 μš”μ²­μ„ 찾을 수 μ—†κ±°λ‚˜ μž‘μ—…μ΄ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
17
+ if (Date.parse(record.expiresAt) <= Date.now()) {
18
+ approvals.delete(id);
19
+ throw new Error("승인 μš”μ²­μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. μž‘μ—… μ΄ˆμ•ˆμ„ λ‹€μ‹œ μƒμ„±ν•˜μ„Έμš”.");
20
+ }
21
+ approvals.delete(id);
22
+ return record;
23
+ }
24
+ export function resetApprovalsForTests() {
25
+ approvals.clear();
26
+ }
27
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/approvals/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;AAEpD,MAAM,UAAU,cAAc,CAAC,MAAgC,EAAE,OAAgC;IAC/F,MAAM,MAAM,GAAmB;QAC7B,EAAE,EAAE,UAAU,EAAE;QAChB,MAAM;QACN,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE;KAC5D,CAAC;IACF,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,MAAgC;IAC1E,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAC3F,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAC/C,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IACD,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACrB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public desktop OAuth client used by the npm distribution.
3
+ *
4
+ * Google treats installed applications as unable to keep a client secret.
5
+ * Set these values once after creating the production Desktop OAuth client.
6
+ * Until then, developers can use credentials.json or environment variables.
7
+ */
8
+ export declare const BUNDLED_GOOGLE_OAUTH_CLIENT: {
9
+ readonly clientId: "749884149912-4nl430614qdfm27clnqtj0imvvfr7j5g.apps.googleusercontent.com";
10
+ readonly clientSecret: "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";
11
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public desktop OAuth client used by the npm distribution.
3
+ *
4
+ * Google treats installed applications as unable to keep a client secret.
5
+ * Set these values once after creating the production Desktop OAuth client.
6
+ * Until then, developers can use credentials.json or environment variables.
7
+ */
8
+ export const BUNDLED_GOOGLE_OAUTH_CLIENT = {
9
+ clientId: "749884149912-4nl430614qdfm27clnqtj0imvvfr7j5g.apps.googleusercontent.com",
10
+ clientSecret: "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"
11
+ };
12
+ //# sourceMappingURL=bundled-oauth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-oauth-client.js","sourceRoot":"","sources":["../../src/auth/bundled-oauth-client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,QAAQ,EAAE,0EAA0E;IACpF,YAAY,EAAE,qCAAqC;CAC3C,CAAC"}
@@ -0,0 +1,12 @@
1
+ export type AuthStatus = {
2
+ authenticated: boolean;
3
+ oauthClientConfigured: boolean;
4
+ oauthClientSource: "environment" | "credentials_file" | "bundled" | "missing";
5
+ credentialsPath: string;
6
+ tokenPath: string;
7
+ grantedScopes: string[];
8
+ message: string;
9
+ };
10
+ export declare function getAuthStatus(): Promise<AuthStatus>;
11
+ export declare function login(): Promise<AuthStatus>;
12
+ export declare function getAuthorizedClient(): Promise<import("googleapis-common").OAuth2Client>;
@@ -0,0 +1,145 @@
1
+ import { createServer } from "node:http";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { google } from "googleapis";
5
+ import { credentialsPath, GOOGLE_SCOPES, tokenPath } from "../config.js";
6
+ import { BUNDLED_GOOGLE_OAUTH_CLIENT } from "./bundled-oauth-client.js";
7
+ async function readJson(filePath) {
8
+ return JSON.parse(await readFile(filePath, "utf8"));
9
+ }
10
+ async function loadClientConfig() {
11
+ const environmentClientId = process.env.EDU_WORKSPACE_GOOGLE_CLIENT_ID?.trim();
12
+ if (environmentClientId) {
13
+ return {
14
+ source: "environment",
15
+ config: {
16
+ client_id: environmentClientId,
17
+ client_secret: process.env.EDU_WORKSPACE_GOOGLE_CLIENT_SECRET?.trim() ?? "",
18
+ redirect_uris: ["http://127.0.0.1"]
19
+ }
20
+ };
21
+ }
22
+ try {
23
+ const credentials = (await readJson(credentialsPath()));
24
+ const clientConfig = credentials.installed ?? credentials.web;
25
+ if (clientConfig)
26
+ return { config: clientConfig, source: "credentials_file" };
27
+ }
28
+ catch {
29
+ // Continue to the bundled public client.
30
+ }
31
+ if (BUNDLED_GOOGLE_OAUTH_CLIENT.clientId) {
32
+ return {
33
+ source: "bundled",
34
+ config: {
35
+ client_id: BUNDLED_GOOGLE_OAUTH_CLIENT.clientId,
36
+ client_secret: BUNDLED_GOOGLE_OAUTH_CLIENT.clientSecret,
37
+ redirect_uris: ["http://127.0.0.1"]
38
+ }
39
+ };
40
+ }
41
+ throw new Error("곡용 Google OAuth 앱이 아직 κ΅¬μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. κ°œλ°œμžλŠ” credentials.json λ˜λŠ” EDU_WORKSPACE_GOOGLE_CLIENT_IDλ₯Ό μ„€μ •ν•˜μ„Έμš”.");
42
+ }
43
+ function createOAuthClient(config, redirectUri) {
44
+ return new google.auth.OAuth2(config.client_id, config.client_secret, redirectUri ?? config.redirect_uris[0]);
45
+ }
46
+ export async function getAuthStatus() {
47
+ const credentialFile = credentialsPath();
48
+ const tokenFile = tokenPath();
49
+ let oauthClientSource = "missing";
50
+ try {
51
+ oauthClientSource = (await loadClientConfig()).source;
52
+ }
53
+ catch {
54
+ // Report missing configuration below.
55
+ }
56
+ try {
57
+ if (oauthClientSource === "missing")
58
+ throw new Error("OAuth client missing");
59
+ const token = (await readJson(tokenFile));
60
+ return {
61
+ authenticated: true,
62
+ oauthClientConfigured: true,
63
+ oauthClientSource,
64
+ credentialsPath: credentialFile,
65
+ tokenPath: tokenFile,
66
+ grantedScopes: token.scope?.split(" ").filter(Boolean) ?? [],
67
+ message: "Google 계정이 μ—°κ²°λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€."
68
+ };
69
+ }
70
+ catch {
71
+ return {
72
+ authenticated: false,
73
+ oauthClientConfigured: oauthClientSource !== "missing",
74
+ oauthClientSource,
75
+ credentialsPath: credentialFile,
76
+ tokenPath: tokenFile,
77
+ grantedScopes: [],
78
+ message: oauthClientSource === "missing"
79
+ ? "곡용 Google OAuth 앱이 아직 κ΅¬μ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 개발용 credentials.json λ˜λŠ” ν™˜κ²½ λ³€μˆ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€."
80
+ : "Google 계정이 μ—°κ²°λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. edu-workspace-mcp login을 μ‹€ν–‰ν•˜μ„Έμš”."
81
+ };
82
+ }
83
+ }
84
+ export async function login() {
85
+ const credentialFile = credentialsPath();
86
+ const tokenFile = tokenPath();
87
+ const { config } = await loadClientConfig();
88
+ const callbackServer = createServer();
89
+ const authorizationCode = new Promise((resolve, reject) => {
90
+ callbackServer.once("request", (request, response) => {
91
+ const callbackUrl = new URL(request.url ?? "/", "http://127.0.0.1");
92
+ const error = callbackUrl.searchParams.get("error");
93
+ const code = callbackUrl.searchParams.get("code");
94
+ if (error || !code) {
95
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
96
+ response.end("Google λ‘œκ·ΈμΈμ„ μ™„λ£Œν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. ν„°λ―Έλ„μ—μ„œ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”.");
97
+ reject(new Error(error ? `Google OAuth 였λ₯˜: ${error}` : "OAuth μ½œλ°±μ— 인증 μ½”λ“œκ°€ μ—†μŠ΅λ‹ˆλ‹€."));
98
+ return;
99
+ }
100
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
101
+ response.end("둜그인이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이 창을 λ‹«κ³  ν„°λ―Έλ„λ‘œ λŒμ•„κ°€μ„Έμš”.");
102
+ resolve(code);
103
+ });
104
+ });
105
+ await new Promise((resolve, reject) => {
106
+ callbackServer.once("error", reject);
107
+ callbackServer.listen(0, "127.0.0.1", () => resolve());
108
+ });
109
+ const address = callbackServer.address();
110
+ if (!address || typeof address === "string") {
111
+ callbackServer.close();
112
+ throw new Error("OAuth 콜백 μ„œλ²„μ˜ 포트λ₯Ό ν• λ‹Ήν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.");
113
+ }
114
+ const redirectUri = `http://127.0.0.1:${address.port}/oauth2callback`;
115
+ const client = createOAuthClient(config, redirectUri);
116
+ const authUrl = client.generateAuthUrl({
117
+ access_type: "offline",
118
+ prompt: "consent",
119
+ scope: [...GOOGLE_SCOPES]
120
+ });
121
+ console.log("λΈŒλΌμš°μ €μ—μ„œ μ•„λž˜ URL을 μ—΄μ–΄ Google λ‘œκ·ΈμΈμ„ μ™„λ£Œν•˜μ„Έμš”:\n");
122
+ console.log(authUrl);
123
+ let code;
124
+ try {
125
+ code = await authorizationCode;
126
+ }
127
+ finally {
128
+ callbackServer.close();
129
+ }
130
+ const { tokens } = await client.getToken(code);
131
+ if (!tokens.refresh_token) {
132
+ throw new Error("κ°±μ‹  토큰을 λ°›μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. Google κ³„μ •μ—μ„œ κΈ°μ‘΄ μ•± κΆŒν•œμ„ ν•΄μ œν•œ λ’€ λ‹€μ‹œ λ‘œκ·ΈμΈν•˜μ„Έμš”.");
133
+ }
134
+ await mkdir(path.dirname(tokenFile), { recursive: true });
135
+ await writeFile(tokenFile, JSON.stringify(tokens, null, 2), { mode: 0o600 });
136
+ return getAuthStatus();
137
+ }
138
+ export async function getAuthorizedClient() {
139
+ const tokenFile = tokenPath();
140
+ const { config } = await loadClientConfig();
141
+ const client = createOAuthClient(config);
142
+ client.setCredentials((await readJson(tokenFile)));
143
+ return client;
144
+ }
145
+ //# sourceMappingURL=google-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-auth.js","sourceRoot":"","sources":["../../src/auth/google-auth.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import { APP_DIRECTORY, credentialsPath, tokenPath } from "./config.js";
4
+ import { getAuthStatus, getAuthorizedClient, login } from "./auth/google-auth.js";
5
+ function printHelp() {
6
+ console.log(`
7
+ edu-workspace-mcp
8
+
9
+ λͺ…λ Ήμ–΄:
10
+ setup 초기 폴더와 MCP μ„€μ • μ˜ˆμ‹œλ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€.
11
+ login Google OAuth 둜그인 ν›„ 토큰을 μ•ˆμ „ν•œ 둜컬 κ²½λ‘œμ— μ €μž₯ν•©λ‹ˆλ‹€.
12
+ doctor OAuth 자격 증λͺ…·토큰 μƒνƒœλ₯Ό ν™•μΈν•©λ‹ˆλ‹€.
13
+ disconnect μ—°κ²° ν•΄μ œ 방법을 μ•ˆλ‚΄ν•©λ‹ˆλ‹€.
14
+ serve stdio MCP μ„œλ²„λ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€. λͺ…령이 없을 λ•Œμ˜ κΈ°λ³Έ λ™μž‘μž…λ‹ˆλ‹€.
15
+ `);
16
+ }
17
+ async function main() {
18
+ const command = process.argv[2] ?? "serve";
19
+ if (command === "serve") {
20
+ await import("./index.js");
21
+ return;
22
+ }
23
+ if (command === "setup") {
24
+ await mkdir(APP_DIRECTORY, { recursive: true });
25
+ const status = await getAuthStatus();
26
+ console.log(`\nμ„€μ • 폴더: ${APP_DIRECTORY}`);
27
+ console.log(`OAuth ν΄λΌμ΄μ–ΈνŠΈ: ${status.oauthClientConfigured ? `${status.oauthClientSource} μ„€μ • μ‚¬μš©` : "아직 κ΅¬μ„±λ˜μ§€ μ•ŠμŒ"}`);
28
+ console.log(`\nCodex/ChatGPT Desktop μ˜ˆμ‹œ:\n[mcp_servers.edu_workspace]\ncommand = "npx"\nargs = ["-y", "edu-workspace-mcp"]`);
29
+ console.log(`\nClaude Desktop/Cursor μ˜ˆμ‹œ:\n{\n "mcpServers": {\n "edu-workspace": {\n "command": "npx",\n "args": ["-y", "edu-workspace-mcp"]\n }\n }\n}`);
30
+ console.log(status.oauthClientConfigured
31
+ ? "\n`npx edu-workspace-mcp login`으둜 Google 계정을 μ—°κ²°ν•˜μ„Έμš”."
32
+ : `\n곡개 배포 μ „μ—λŠ” λ°°ν¬μžκ°€ 곡용 OAuth 앱을 μ„€μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€. 개발 μ€‘μ—λŠ” ${credentialsPath()}에 credentials.json을 λ‘˜ 수 μžˆμŠ΅λ‹ˆλ‹€.`);
33
+ return;
34
+ }
35
+ if (command === "login") {
36
+ const status = await login();
37
+ console.log(status.message);
38
+ console.log(`토큰 μ €μž₯ μœ„μΉ˜: ${status.tokenPath}`);
39
+ return;
40
+ }
41
+ if (command === "doctor") {
42
+ const status = await getAuthStatus();
43
+ console.log(JSON.stringify(status, null, 2));
44
+ if (status.authenticated) {
45
+ const client = await getAuthorizedClient();
46
+ await client.getAccessToken();
47
+ console.log("Google OAuth 토큰 κ°±μ‹ : 정상");
48
+ }
49
+ return;
50
+ }
51
+ if (command === "disconnect") {
52
+ await rm(tokenPath(), { force: true });
53
+ console.log(`둜컬 Google 토큰을 μ œκ±°ν–ˆμŠ΅λ‹ˆλ‹€: ${tokenPath()}`);
54
+ console.log("Google κ³„μ •μ˜ 제3자 μ•± μ•‘μ„ΈμŠ€ νŽ˜μ΄μ§€μ—μ„œλ„ 이 μ•±μ˜ κΆŒν•œμ„ μ² νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.");
55
+ return;
56
+ }
57
+ printHelp();
58
+ }
59
+ main().catch((error) => {
60
+ console.error(error instanceof Error ? error.message : error);
61
+ process.exitCode = 1;
62
+ });
63
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare const APP_NAME = "edu-workspace-mcp";
2
+ export declare const APP_DIRECTORY: string;
3
+ export declare const DEFAULT_CREDENTIALS_PATH: string;
4
+ export declare const DEFAULT_TOKEN_PATH: string;
5
+ export declare const GOOGLE_SCOPES: readonly ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/classroom.courses.readonly", "https://www.googleapis.com/auth/classroom.coursework.me"];
6
+ export declare function credentialsPath(): string;
7
+ export declare function tokenPath(): string;
package/dist/config.js ADDED
@@ -0,0 +1,18 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export const APP_NAME = "edu-workspace-mcp";
4
+ export const APP_DIRECTORY = path.join(os.homedir(), `.${APP_NAME}`);
5
+ export const DEFAULT_CREDENTIALS_PATH = path.join(APP_DIRECTORY, "credentials.json");
6
+ export const DEFAULT_TOKEN_PATH = path.join(APP_DIRECTORY, "token.json");
7
+ export const GOOGLE_SCOPES = [
8
+ "https://www.googleapis.com/auth/drive.file",
9
+ "https://www.googleapis.com/auth/classroom.courses.readonly",
10
+ "https://www.googleapis.com/auth/classroom.coursework.me"
11
+ ];
12
+ export function credentialsPath() {
13
+ return process.env.EDU_WORKSPACE_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH;
14
+ }
15
+ export function tokenPath() {
16
+ return process.env.EDU_WORKSPACE_TOKEN_PATH ?? DEFAULT_TOKEN_PATH;
17
+ }
18
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,QAAQ,GAAG,mBAAmB,CAAC;AAE5C,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;AACrF,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,4CAA4C;IAC5C,4DAA4D;IAC5D,yDAAyD;CACjD,CAAC;AAEX,MAAM,UAAU,eAAe;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,wBAAwB,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,kBAAkB,CAAC;AACpE,CAAC"}
@@ -0,0 +1,39 @@
1
+ export type CourseSummary = {
2
+ id: string;
3
+ name: string;
4
+ section?: string;
5
+ state?: string;
6
+ alternateLink?: string;
7
+ };
8
+ export declare function listCourses(query?: string): Promise<CourseSummary[]>;
9
+ export type AssignmentDraftInput = {
10
+ courseId: string;
11
+ title: string;
12
+ description?: string;
13
+ dueAt?: string;
14
+ maxPoints?: number;
15
+ materials?: Array<{
16
+ title: string;
17
+ url: string;
18
+ }>;
19
+ };
20
+ export declare function createAssignmentDraft(input: AssignmentDraftInput): Promise<{
21
+ courseId: string;
22
+ courseWorkId: string | null | undefined;
23
+ title: string | null | undefined;
24
+ state: string | null | undefined;
25
+ alternateLink: string | null | undefined;
26
+ dueDate: import("googleapis").classroom_v1.Schema$Date | undefined;
27
+ dueTime: import("googleapis").classroom_v1.Schema$TimeOfDay | undefined;
28
+ materials: {
29
+ title: string;
30
+ url: string;
31
+ }[];
32
+ }>;
33
+ export declare function publishAssignment(courseId: string, courseWorkId: string): Promise<{
34
+ courseId: string;
35
+ courseWorkId: string;
36
+ state: string | null | undefined;
37
+ title: string | null | undefined;
38
+ alternateLink: string | null | undefined;
39
+ }>;