lex-microsoft_teams 0.6.47 → 0.6.50

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +21 -1
  4. data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +2 -1
  5. data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +2 -1
  6. data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +4 -2
  7. data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +1 -1
  8. data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +2 -0
  9. data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +17 -7
  10. data/lib/legion/extensions/microsoft_teams/runners/call_events.rb +71 -11
  11. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +68 -12
  12. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +12 -4
  13. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +44 -5
  14. data/lib/legion/extensions/microsoft_teams/runners/files.rb +77 -9
  15. data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +2 -0
  16. data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +33 -5
  17. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +35 -4
  18. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +76 -13
  19. data/lib/legion/extensions/microsoft_teams/runners/ownership.rb +1 -0
  20. data/lib/legion/extensions/microsoft_teams/runners/people.rb +14 -2
  21. data/lib/legion/extensions/microsoft_teams/runners/presence.rb +1 -0
  22. data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +7 -1
  23. data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +1 -0
  24. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +47 -8
  25. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +36 -6
  26. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  27. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f0e5b03a3a863f2faca183a00131d28004da19acd329c8d50a9682e45ea150b
4
- data.tar.gz: 95ebc57ef21c39b5245154154a1d0ba439a7a5a5fb4ccd9b89eb92f2ba800559
3
+ metadata.gz: 35010357ecbf55560b6957ae2467c5975eec04056da102a34b1d4c870a0f1ce3
4
+ data.tar.gz: 38d375c241ba2691948bf6d80f275410f0a1003e0e7456852f3bc2ca1bf4acce
5
5
  SHA512:
6
- metadata.gz: '084715b8ddefcd486bab4a566507be13f455f2ebccd334529d4fd81fc6c567ff258dc00672f40601b03262792a79364d10a0f835cd2df8d26dfd40ba7ede72d7'
7
- data.tar.gz: e74bf561e91a2ebfe4c178a52ec3678b5a65a30fb0af7cd3fbdcc0ebbbd1a698d58a5fea9c8fc7f9a263b14f5e202cc91accf57f67280285d2e2221c628f137c
6
+ metadata.gz: ae71a656141975e85dc7b5e294e99d7d5a1f67a34e57feea943f177d4bf1c31224ef462adc5140163f6cb028ca1fede816fa713c25185522d8718fe01631a0af
7
+ data.tar.gz: b2e865c65531f7d464d7f058c2e8d965b1ca96643ca15bee7a6018c0dd260d7477771b0418404f9a6dd0ef8fc8dd983d8c68d0a06704fb2432af031d377150d6
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  /Gemfile.lock
10
+ *.gem
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
data/CHANGELOG.md CHANGED
@@ -1,8 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.6.50] - 2026-05-27
4
+ ### Added
5
+ - Full OData query parameter support across all Graph API runner methods per Microsoft Graph REST v1.0 docs
6
+ - `max_pages` pagination parameter on all list endpoints — follows `@odata.nextLink` automatically to fetch multiple pages in a single call
7
+ - `$top` exposed in MCP inputs for: list_chat_messages, list_chats, list_channel_messages, list_channel_message_replies, list_message_replies, list_team_members, list_meetings, list_drive_items, list_team_drive_items, list_call_sessions, list_session_segments, list_meeting_artifacts, list_transcripts
8
+ - `$orderby` support for list_chat_messages (lastModifiedDateTime desc, createdDateTime desc) and list_chats (lastMessagePreview/createdDateTime desc)
9
+ - `$filter` support for list_chat_messages, list_chats, list_channels, list_joined_teams, list_team_members, list_meetings, list_drive_items, list_people
10
+ - `$expand` support for list_chats (members, lastMessagePreview), list_channel_messages (replies), list_installed_apps_for_user, list_installed_apps_in_chat, list_call_sessions (segments)
11
+ - `$select` support for list_channels, list_joined_teams, list_drive_items, list_team_drive_items, list_call_sessions, list_people
12
+ - `$search` support for list_people
13
+ - `format` (vtt/docx) exposed in MCP inputs for get_transcript_content
14
+
15
+ ### Fixed
16
+ - Per-page size capped at Graph API maximum (50 for messages/chats, 200 for drive items) regardless of `top` value passed
17
+
18
+ ## [0.6.48] - 2026-05-18
19
+ ### Added
20
+ - Definition DSL declarations across all runners for proper tool discovery and MCP exposure
21
+ - Fix transformer API: map definition[:prompt] to transform(transformation:) for lex-transformer 0.3.8
22
+
4
23
  ### Fixed
5
24
  - Bot response and observation extraction now pass explicit system and user messages to native `Legion::LLM.chat` dispatch instead of routing through the legacy nil-input `llm_chat` helper path.
25
+ - Runner modules that declare `definition` now explicitly `extend Legion::Extensions::Definitions` before those DSL calls, fixing `NoMethodError: undefined method 'definition'` during Legion extension boot.
6
26
 
7
27
  ## [0.6.45] - 2026-04-23
8
28
 
@@ -168,7 +168,8 @@ module Legion
168
168
  tags: ['teams', 'channel', 'thread', channel_name],
169
169
  source_file: "teams://teams/#{team_id}/channels/#{channel_id}/messages/#{msg_id}",
170
170
  heading: "Channel Thread: #{channel_name}#{" — #{subject}" if subject}",
171
- content_type: 'teams_channel_thread'
171
+ content_type: 'teams_channel_thread',
172
+ access_scope: 'private'
172
173
  )
173
174
  results[:chunks] += 1
174
175
  rescue StandardError => e
@@ -116,7 +116,8 @@ module Legion
116
116
  tags: ['teams', 'chat', 'messages', topic],
117
117
  source_file: "teams://chats/#{chat_id}/messages",
118
118
  heading: "Chat: #{topic}",
119
- content_type: 'teams_chat_thread'
119
+ content_type: 'teams_chat_thread',
120
+ access_scope: 'private'
120
121
  )
121
122
  results[:chunks] += 1
122
123
  log.info("Chat#ingest_messages stored #{lines.length} lines for chat_id=#{chat_id}")
@@ -134,7 +134,8 @@ module Legion
134
134
  tags: ['meeting', 'transcript', subject],
135
135
  source_file: "teams://meetings/#{meeting_id}/transcripts/#{transcript_id}",
136
136
  heading: "Transcript: #{subject}",
137
- content_type: 'meeting_transcript'
137
+ content_type: 'meeting_transcript',
138
+ access_scope: 'private'
138
139
  )
139
140
  results[:chunks] += 1
140
141
  end
@@ -174,7 +175,8 @@ module Legion
174
175
  tags: ['meeting', 'ai-insight', 'action-item', subject],
175
176
  source_file: "teams://meetings/#{meeting_id}/insights/#{insight_id}",
176
177
  heading: "AI Insight: #{subject}",
177
- content_type: 'meeting_insight'
178
+ content_type: 'meeting_insight',
179
+ access_scope: 'private'
178
180
  )
179
181
  results[:chunks] += 1
180
182
  end
@@ -36,7 +36,7 @@ module Legion
36
36
  end
37
37
 
38
38
  def enabled?
39
- channel_setting(:enabled, false) == true
39
+ # channel_setting(:enabled, false) == true
40
40
  rescue StandardError => e
41
41
  handle_exception(e, level: :debug, operation: 'ChannelPoller#enabled?')
42
42
  false
@@ -327,6 +327,7 @@ module Legion
327
327
  source_agent: 'teams-api-ingest',
328
328
  source_provider: 'microsoft',
329
329
  source_channel: 'teams_graph_api',
330
+ access_scope: 'private',
330
331
  context: { person: person_name, message_count: texts.length }
331
332
  )
332
333
  ingested += 1 if result[:success]
@@ -356,6 +357,7 @@ module Legion
356
357
  source_agent: 'teams-entity-extractor',
357
358
  source_provider: 'microsoft',
358
359
  source_channel: 'teams_graph_api',
360
+ access_scope: 'private',
359
361
  context: { entity_name: entity[:name], entity_type: entity[:type],
360
362
  confidence: entity[:confidence], extracted_from: person_name }
361
363
  )
@@ -15,29 +15,39 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_installed_apps_for_user,
18
- desc: 'List Teams apps installed for a user',
18
+ desc: 'List Teams apps installed for a user with expand support',
19
19
  mcp_prefix: 'teams.list_installed_apps_for_user',
20
20
  mcp_category: 'teams_apps',
21
21
  mcp_tier: :low,
22
22
  idempotent: true,
23
+ inputs: { properties: { expand: { type: 'string',
24
+ description: 'Expand related entities (e.g. teamsApp)' } },
25
+ required: [] },
23
26
  trigger_words: %w[apps installed]
24
27
 
25
- def list_installed_apps_for_user(user_id: 'me', **)
26
- response = graph_connection(**).get("#{user_path(user_id)}/teamwork/installedApps")
28
+ def list_installed_apps_for_user(user_id: 'me', expand: nil, **)
29
+ params = {}
30
+ params['$expand'] = expand if expand
31
+ response = graph_connection(**).get("#{user_path(user_id)}/teamwork/installedApps", params)
27
32
  { result: response.body }
28
33
  end
29
34
 
30
35
  definition :list_installed_apps_in_chat,
31
- desc: 'List Teams apps installed in a specific chat',
36
+ desc: 'List Teams apps installed in a specific chat with expand support',
32
37
  mcp_prefix: 'teams.list_installed_apps_in_chat',
33
38
  mcp_category: 'teams_apps',
34
39
  mcp_tier: :low,
35
40
  idempotent: true,
36
- inputs: { properties: { chat_id: { type: 'string' } }, required: ['chat_id'] },
41
+ inputs: { properties: { chat_id: { type: 'string' },
42
+ expand: { type: 'string',
43
+ description: 'Expand related entities (e.g. teamsApp)' } },
44
+ required: ['chat_id'] },
37
45
  trigger_words: %w[apps chat]
38
46
 
39
- def list_installed_apps_in_chat(chat_id:, **)
40
- response = graph_connection(**).get("chats/#{chat_id}/installedApps")
47
+ def list_installed_apps_in_chat(chat_id:, expand: nil, **)
48
+ params = {}
49
+ params['$expand'] = expand if expand
50
+ response = graph_connection(**).get("chats/#{chat_id}/installedApps", params)
41
51
  { result: response.body }
42
52
  end
43
53
 
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module CallEvents
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,17 +15,50 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_call_sessions,
17
- desc: 'List sessions for a Teams call record',
18
+ desc: 'List sessions for a Teams call record with pagination and expand',
18
19
  mcp_prefix: 'teams.list_call_sessions',
19
20
  mcp_category: 'teams_calls',
20
21
  mcp_tier: :standard,
21
22
  idempotent: true,
22
- inputs: { properties: { call_id: { type: 'string' } }, required: ['call_id'] },
23
+ inputs: { properties: { call_id: { type: 'string' },
24
+ top: { type: 'integer',
25
+ description: 'Sessions per page (default 50)' },
26
+ max_pages: { type: 'integer',
27
+ description: 'Maximum pages to fetch (default 1)' },
28
+ expand: { type: 'string',
29
+ description: 'Expand related entities (e.g. segments)' },
30
+ select: { type: 'string',
31
+ description: 'Comma-separated fields to return' } },
32
+ required: ['call_id'] },
23
33
  trigger_words: %w[sessions calls records]
24
34
 
25
- def list_call_sessions(call_id:, **)
26
- response = graph_connection(**).get("communications/callRecords/#{call_id}/sessions")
27
- { result: response.body }
35
+ def list_call_sessions(call_id:, top: 50, max_pages: 1, expand: nil, select: nil, **)
36
+ params = { '$top' => top }
37
+ params['$expand'] = expand if expand
38
+ params['$select'] = select if select
39
+ conn = graph_connection(**)
40
+ response = conn.get("communications/callRecords/#{call_id}/sessions", params)
41
+ body = response.body
42
+
43
+ return { result: body } if max_pages <= 1
44
+
45
+ all_values = Array(body['value'] || body[:value])
46
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
47
+ pages_fetched = 1
48
+
49
+ while next_link && pages_fetched < max_pages
50
+ response = conn.get(next_link)
51
+ page_body = response.body
52
+ items = page_body['value'] || page_body[:value]
53
+ all_values.concat(Array(items)) if items
54
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
55
+ pages_fetched += 1
56
+ end
57
+
58
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
59
+ 'value' => all_values }
60
+ result['@odata.nextLink'] = next_link if next_link
61
+ { result: result }
28
62
  end
29
63
 
30
64
  definition :get_call_session,
@@ -46,21 +80,47 @@ module Legion
46
80
  end
47
81
 
48
82
  definition :list_session_segments,
49
- desc: 'List segments for a session in a Teams call record',
83
+ desc: 'List segments for a session in a Teams call record with pagination',
50
84
  mcp_prefix: 'teams.list_session_segments',
51
85
  mcp_category: 'teams_calls',
52
86
  mcp_tier: :standard,
53
87
  idempotent: true,
54
88
  inputs: { properties: { call_id: { type: 'string' },
55
- session_id: { type: 'string' } },
89
+ session_id: { type: 'string' },
90
+ top: { type: 'integer',
91
+ description: 'Segments per page (default 50)' },
92
+ max_pages: { type: 'integer',
93
+ description: 'Maximum pages to fetch (default 1)' } },
56
94
  required: %w[call_id session_id] },
57
95
  trigger_words: %w[segments pstn]
58
96
 
59
- def list_session_segments(call_id:, session_id:, **)
60
- response = graph_connection(**).get(
61
- "communications/callRecords/#{call_id}/sessions/#{session_id}/segments"
97
+ def list_session_segments(call_id:, session_id:, top: 50, max_pages: 1, **)
98
+ params = { '$top' => top }
99
+ conn = graph_connection(**)
100
+ response = conn.get(
101
+ "communications/callRecords/#{call_id}/sessions/#{session_id}/segments", params
62
102
  )
63
- { result: response.body }
103
+ body = response.body
104
+
105
+ return { result: body } if max_pages <= 1
106
+
107
+ all_values = Array(body['value'] || body[:value])
108
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
109
+ pages_fetched = 1
110
+
111
+ while next_link && pages_fetched < max_pages
112
+ response = conn.get(next_link)
113
+ page_body = response.body
114
+ items = page_body['value'] || page_body[:value]
115
+ all_values.concat(Array(items)) if items
116
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
117
+ pages_fetched += 1
118
+ end
119
+
120
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
121
+ 'value' => all_values }
122
+ result['@odata.nextLink'] = next_link if next_link
123
+ { result: result }
64
124
  end
65
125
 
66
126
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module ChannelMessages
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,20 +15,49 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_channel_messages,
17
- desc: 'List messages posted in a Teams channel',
18
+ desc: 'List messages posted in a Teams channel with pagination and expand support',
18
19
  mcp_prefix: 'teams.list_channel_messages',
19
20
  mcp_category: 'teams_channel_messages',
20
21
  mcp_tier: :standard,
21
22
  idempotent: true,
22
23
  inputs: { properties: { team_id: { type: 'string' },
23
- channel_id: { type: 'string' } },
24
+ channel_id: { type: 'string' },
25
+ top: { type: 'integer',
26
+ description: 'Messages per page (default 20, max 50)' },
27
+ max_pages: { type: 'integer',
28
+ description: 'Maximum pages to fetch (default 1)' },
29
+ expand: { type: 'string',
30
+ description: 'Expand related entities (e.g. replies)' } },
24
31
  required: %w[team_id channel_id] },
25
32
  trigger_words: %w[channel history posts feed]
26
33
 
27
- def list_channel_messages(team_id:, channel_id:, top: 50, **)
28
- params = { '$top' => top }
29
- response = graph_connection(**).get("teams/#{team_id}/channels/#{channel_id}/messages", params)
30
- { result: response.body }
34
+ def list_channel_messages(team_id:, channel_id:, top: 50, max_pages: 1, expand: nil, **)
35
+ per_page = [top, 50].min
36
+ params = { '$top' => per_page }
37
+ params['$expand'] = expand if expand
38
+ conn = graph_connection(**)
39
+ response = conn.get("teams/#{team_id}/channels/#{channel_id}/messages", params)
40
+ body = response.body
41
+
42
+ return { result: body } if max_pages <= 1
43
+
44
+ all_values = Array(body['value'] || body[:value])
45
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
46
+ pages_fetched = 1
47
+
48
+ while next_link && pages_fetched < max_pages
49
+ response = conn.get(next_link)
50
+ page_body = response.body
51
+ items = page_body['value'] || page_body[:value]
52
+ all_values.concat(Array(items)) if items
53
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
54
+ pages_fetched += 1
55
+ end
56
+
57
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
58
+ 'value' => all_values }
59
+ result['@odata.nextLink'] = next_link if next_link
60
+ { result: result }
31
61
  end
32
62
 
33
63
  definition :get_channel_message,
@@ -88,23 +118,49 @@ module Legion
88
118
  end
89
119
 
90
120
  definition :list_channel_message_replies,
91
- desc: 'List replies in a Teams channel message thread',
121
+ desc: 'List replies in a Teams channel message thread with pagination',
92
122
  mcp_prefix: 'teams.list_channel_message_replies',
93
123
  mcp_category: 'teams_channel_messages',
94
124
  mcp_tier: :standard,
95
125
  idempotent: true,
96
126
  inputs: { properties: { team_id: { type: 'string' },
97
127
  channel_id: { type: 'string' },
98
- message_id: { type: 'string' } },
128
+ message_id: { type: 'string' },
129
+ top: { type: 'integer',
130
+ description: 'Replies per page (default 50, max 50)' },
131
+ max_pages: { type: 'integer',
132
+ description: 'Maximum pages to fetch (default 1)' } },
99
133
  required: %w[team_id channel_id message_id] },
100
134
  trigger_words: %w[replies thread]
101
135
 
102
- def list_channel_message_replies(team_id:, channel_id:, message_id:, top: 50, **)
103
- params = { '$top' => top }
104
- response = graph_connection(**).get(
136
+ def list_channel_message_replies(team_id:, channel_id:, message_id:, top: 50, max_pages: 1, **)
137
+ per_page = [top, 50].min
138
+ params = { '$top' => per_page }
139
+ conn = graph_connection(**)
140
+ response = conn.get(
105
141
  "teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}/replies", params
106
142
  )
107
- { result: response.body }
143
+ body = response.body
144
+
145
+ return { result: body } if max_pages <= 1
146
+
147
+ all_values = Array(body['value'] || body[:value])
148
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
149
+ pages_fetched = 1
150
+
151
+ while next_link && pages_fetched < max_pages
152
+ response = conn.get(next_link)
153
+ page_body = response.body
154
+ items = page_body['value'] || page_body[:value]
155
+ all_values.concat(Array(items)) if items
156
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
157
+ pages_fetched += 1
158
+ end
159
+
160
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
161
+ 'value' => all_values }
162
+ result['@odata.nextLink'] = next_link if next_link
163
+ { result: result }
108
164
  end
109
165
 
110
166
  definition :edit_channel_message,
@@ -15,16 +15,24 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_channels,
18
- desc: 'List channels in a Team',
18
+ desc: 'List channels in a Team with optional filtering and select',
19
19
  mcp_prefix: 'teams.list_channels',
20
20
  mcp_category: 'teams_channels',
21
21
  mcp_tier: :low,
22
22
  idempotent: true,
23
- inputs: { properties: { team_id: { type: 'string' } }, required: ['team_id'] },
23
+ inputs: { properties: { team_id: { type: 'string' },
24
+ filter: { type: 'string',
25
+ description: 'OData $filter (e.g. membershipType eq \'standard\')' },
26
+ select: { type: 'string',
27
+ description: 'Comma-separated fields to return' } },
28
+ required: ['team_id'] },
24
29
  trigger_words: %w[channels list]
25
30
 
26
- def list_channels(team_id:, **)
27
- response = graph_connection(**).get("teams/#{team_id}/channels")
31
+ def list_channels(team_id:, filter: nil, select: nil, **)
32
+ params = {}
33
+ params['$filter'] = filter if filter
34
+ params['$select'] = select if select
35
+ response = graph_connection(**).get("teams/#{team_id}/channels", params)
28
36
  { result: response.body }
29
37
  end
30
38
 
@@ -15,17 +15,54 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_chats,
18
- desc: 'List Teams chats for the current user',
18
+ desc: 'List Teams chats for the current user with pagination, filtering, and expand support',
19
19
  mcp_prefix: 'teams.list_chats',
20
20
  mcp_category: 'teams_chat',
21
21
  mcp_tier: :standard,
22
22
  idempotent: true,
23
+ inputs: { properties: { top: { type: 'integer',
24
+ description: 'Number of chats per page (default 50, max 50)' },
25
+ max_pages: { type: 'integer',
26
+ description: 'Maximum pages to fetch (default 1)' },
27
+ expand: { type: 'string',
28
+ description: 'Expand related entities: members, lastMessagePreview' },
29
+ filter: { type: 'string',
30
+ description: 'OData $filter expression (e.g. chatType eq \'group\')' },
31
+ orderby: { type: 'string',
32
+ description: 'Sort order (e.g. lastMessagePreview/createdDateTime desc)' } },
33
+ required: [] },
23
34
  trigger_words: %w[chats conversations]
24
35
 
25
- def list_chats(user_id: 'me', top: 50, **)
26
- params = { '$top' => top }
27
- response = graph_connection(**).get("#{user_path(user_id)}/chats", params)
28
- { result: response.body }
36
+ def list_chats(user_id: 'me', top: 50, max_pages: 1, expand: nil, filter: nil, orderby: nil, **)
37
+ log.debug "list_chats(user_id: #{user_id}, top: #{top}, max_pages: #{max_pages})"
38
+ per_page = [top, 50].min
39
+ params = { '$top' => per_page }
40
+ params['$expand'] = expand if expand
41
+ params['$filter'] = filter if filter
42
+ params['$orderby'] = orderby if orderby
43
+ conn = graph_connection(**)
44
+ response = conn.get("#{user_path(user_id)}/chats", params)
45
+ body = response.body
46
+
47
+ return { result: body } if max_pages <= 1
48
+
49
+ all_values = Array(body['value'] || body[:value])
50
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
51
+ pages_fetched = 1
52
+
53
+ while next_link && pages_fetched < max_pages
54
+ response = conn.get(next_link)
55
+ page_body = response.body
56
+ items = page_body['value'] || page_body[:value]
57
+ all_values.concat(Array(items)) if items
58
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
59
+ pages_fetched += 1
60
+ end
61
+
62
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
63
+ 'value' => all_values }
64
+ result['@odata.nextLink'] = next_link if next_link
65
+ { result: result }
29
66
  end
30
67
 
31
68
  definition :get_chat,
@@ -40,6 +77,7 @@ module Legion
40
77
  trigger_words: %w[chat details]
41
78
 
42
79
  def get_chat(chat_id:, **)
80
+ log.debug("get_chat(chat_id: #{chat_id}, **)")
43
81
  response = graph_connection(**).get("chats/#{chat_id}")
44
82
  { result: response.body }
45
83
  end
@@ -74,6 +112,7 @@ module Legion
74
112
  trigger_words: %w[members participants]
75
113
 
76
114
  def list_chat_members(chat_id:, **)
115
+ log.debug "list_chat_members(chat_id: #{chat_id})"
77
116
  response = graph_connection(**).get("chats/#{chat_id}/members")
78
117
  { result: response.body }
79
118
  end
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Files
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,16 +15,50 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_drive_items,
17
- desc: 'List files in the root of a user\'s OneDrive',
18
+ desc: 'List files in the root of a user\'s OneDrive with pagination and filtering',
18
19
  mcp_prefix: 'teams.list_drive_items',
19
20
  mcp_category: 'teams_files',
20
21
  mcp_tier: :standard,
21
22
  idempotent: true,
23
+ inputs: { properties: { top: { type: 'integer',
24
+ description: 'Items per page (default 200)' },
25
+ max_pages: { type: 'integer',
26
+ description: 'Maximum pages to fetch (default 1)' },
27
+ select: { type: 'string',
28
+ description: 'Comma-separated fields to return' },
29
+ filter: { type: 'string',
30
+ description: 'OData $filter expression' } },
31
+ required: [] },
22
32
  trigger_words: %w[files drive onedrive]
23
33
 
24
- def list_drive_items(user_id: 'me', **)
25
- response = graph_connection(**).get("#{user_path(user_id)}/drive/root/children")
26
- { result: response.body }
34
+ def list_drive_items(user_id: 'me', top: 200, max_pages: 1, select: nil, filter: nil, **)
35
+ log.debug "list_drive_items(user_id: #{user_id})"
36
+ params = { '$top' => top }
37
+ params['$select'] = select if select
38
+ params['$filter'] = filter if filter
39
+ conn = graph_connection(**)
40
+ response = conn.get("#{user_path(user_id)}/drive/root/children", params)
41
+ body = response.body
42
+
43
+ return { result: body } if max_pages <= 1
44
+
45
+ all_values = Array(body['value'] || body[:value])
46
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
47
+ pages_fetched = 1
48
+
49
+ while next_link && pages_fetched < max_pages
50
+ response = conn.get(next_link)
51
+ page_body = response.body
52
+ items = page_body['value'] || page_body[:value]
53
+ all_values.concat(Array(items)) if items
54
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
55
+ pages_fetched += 1
56
+ end
57
+
58
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
59
+ 'value' => all_values }
60
+ result['@odata.nextLink'] = next_link if next_link
61
+ { result: result }
27
62
  end
28
63
 
29
64
  definition :get_drive_item,
@@ -36,6 +71,7 @@ module Legion
36
71
  trigger_words: %w[file item]
37
72
 
38
73
  def get_drive_item(item_id:, user_id: 'me', **)
74
+ log.debug "get_drive_item(item_id: #{item_id}), user_id: #{user_id}"
39
75
  response = graph_connection(**).get("#{user_path(user_id)}/drive/items/#{item_id}")
40
76
  { result: response.body }
41
77
  end
@@ -50,22 +86,54 @@ module Legion
50
86
  trigger_words: %w[download content read]
51
87
 
52
88
  def get_drive_item_content(item_id:, user_id: 'me', **)
89
+ log.debug "get_drive_item_content(item_id: #{item_id}, user_id: #{user_id})"
53
90
  response = graph_connection(**).get("#{user_path(user_id)}/drive/items/#{item_id}/content")
54
91
  { result: response.body }
55
92
  end
56
93
 
57
94
  definition :list_team_drive_items,
58
- desc: 'List files in a Team\'s SharePoint document library',
95
+ desc: 'List files in a Team\'s SharePoint document library with pagination',
59
96
  mcp_prefix: 'teams.list_team_drive_items',
60
97
  mcp_category: 'teams_files',
61
98
  mcp_tier: :standard,
62
99
  idempotent: true,
63
- inputs: { properties: { team_id: { type: 'string' } }, required: ['team_id'] },
100
+ inputs: { properties: { team_id: { type: 'string' },
101
+ top: { type: 'integer',
102
+ description: 'Items per page (default 200)' },
103
+ max_pages: { type: 'integer',
104
+ description: 'Maximum pages to fetch (default 1)' },
105
+ select: { type: 'string',
106
+ description: 'Comma-separated fields to return' } },
107
+ required: ['team_id'] },
64
108
  trigger_words: %w[sharepoint documents team]
65
109
 
66
- def list_team_drive_items(team_id:, **)
67
- response = graph_connection(**).get("teams/#{team_id}/drive/root/children")
68
- { result: response.body }
110
+ def list_team_drive_items(team_id:, top: 200, max_pages: 1, select: nil, **)
111
+ log.debug "list_team_drive_items(team_id: #{team_id})"
112
+ params = { '$top' => top }
113
+ params['$select'] = select if select
114
+ conn = graph_connection(**)
115
+ response = conn.get("teams/#{team_id}/drive/root/children", params)
116
+ body = response.body
117
+
118
+ return { result: body } if max_pages <= 1
119
+
120
+ all_values = Array(body['value'] || body[:value])
121
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
122
+ pages_fetched = 1
123
+
124
+ while next_link && pages_fetched < max_pages
125
+ response = conn.get(next_link)
126
+ page_body = response.body
127
+ items = page_body['value'] || page_body[:value]
128
+ all_values.concat(Array(items)) if items
129
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
130
+ pages_fetched += 1
131
+ end
132
+
133
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
134
+ 'value' => all_values }
135
+ result['@odata.nextLink'] = next_link if next_link
136
+ { result: result }
69
137
  end
70
138
 
71
139
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -7,6 +7,8 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module LocalCache
10
+ extend Legion::Extensions::Definitions
11
+
10
12
  definition :extract_local_messages, mcp_exposed: false
11
13
  definition :local_cache_available?, mcp_exposed: false
12
14
  definition :local_cache_stats, mcp_exposed: false
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module MeetingArtifacts
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,17 +15,44 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_meeting_artifacts,
17
- desc: 'List artifacts (recordings, whiteboards) for an online meeting',
18
+ desc: 'List artifacts (recordings, whiteboards) for an online meeting with pagination',
18
19
  mcp_prefix: 'teams.list_meeting_artifacts',
19
20
  mcp_category: 'teams_meetings',
20
21
  mcp_tier: :standard,
21
22
  idempotent: true,
22
- inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
23
+ inputs: { properties: { meeting_id: { type: 'string' },
24
+ top: { type: 'integer',
25
+ description: 'Artifacts per page (default 50)' },
26
+ max_pages: { type: 'integer',
27
+ description: 'Maximum pages to fetch (default 1)' } },
28
+ required: ['meeting_id'] },
23
29
  trigger_words: %w[artifacts recordings whiteboards]
24
30
 
25
- def list_meeting_artifacts(meeting_id:, user_id: 'me', **)
26
- response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/artifacts")
27
- { result: response.body }
31
+ def list_meeting_artifacts(meeting_id:, user_id: 'me', top: 50, max_pages: 1, **)
32
+ params = { '$top' => top }
33
+ conn = graph_connection(**)
34
+ response = conn.get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/artifacts", params)
35
+ body = response.body
36
+
37
+ return { result: body } if max_pages <= 1
38
+
39
+ all_values = Array(body['value'] || body[:value])
40
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
41
+ pages_fetched = 1
42
+
43
+ while next_link && pages_fetched < max_pages
44
+ response = conn.get(next_link)
45
+ page_body = response.body
46
+ items = page_body['value'] || page_body[:value]
47
+ all_values.concat(Array(items)) if items
48
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
49
+ pages_fetched += 1
50
+ end
51
+
52
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
53
+ 'value' => all_values }
54
+ result['@odata.nextLink'] = next_link if next_link
55
+ { result: result }
28
56
  end
29
57
 
30
58
  definition :get_meeting_artifact,
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Meetings
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,16 +15,46 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_meetings,
17
- desc: 'List online meetings for the current user',
18
+ desc: 'List online meetings for the current user with pagination and filtering',
18
19
  mcp_prefix: 'teams.list_meetings',
19
20
  mcp_category: 'teams_meetings',
20
21
  mcp_tier: :low,
21
22
  idempotent: true,
23
+ inputs: { properties: { top: { type: 'integer',
24
+ description: 'Meetings per page (default 50)' },
25
+ max_pages: { type: 'integer',
26
+ description: 'Maximum pages to fetch (default 1)' },
27
+ filter: { type: 'string',
28
+ description: 'OData $filter expression' } },
29
+ required: [] },
22
30
  trigger_words: %w[meetings upcoming calendar]
23
31
 
24
- def list_meetings(user_id: 'me', **)
25
- response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings")
26
- { result: response.body }
32
+ def list_meetings(user_id: 'me', top: 50, max_pages: 1, filter: nil, **)
33
+ params = { '$top' => top }
34
+ params['$filter'] = filter if filter
35
+ conn = graph_connection(**)
36
+ response = conn.get("#{user_path(user_id)}/onlineMeetings", params)
37
+ body = response.body
38
+
39
+ return { result: body } if max_pages <= 1
40
+
41
+ all_values = Array(body['value'] || body[:value])
42
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
43
+ pages_fetched = 1
44
+
45
+ while next_link && pages_fetched < max_pages
46
+ response = conn.get(next_link)
47
+ page_body = response.body
48
+ items = page_body['value'] || page_body[:value]
49
+ all_values.concat(Array(items)) if items
50
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
51
+ pages_fetched += 1
52
+ end
53
+
54
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
55
+ 'value' => all_values }
56
+ result['@odata.nextLink'] = next_link if next_link
57
+ { result: result }
27
58
  end
28
59
 
29
60
  definition :get_meeting,
@@ -15,20 +15,53 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_chat_messages,
18
- desc: 'List messages in a Teams chat thread',
18
+ desc: 'List messages in a Teams chat thread with pagination, ordering, and filtering',
19
19
  mcp_prefix: 'teams.list_chat_messages',
20
20
  mcp_category: 'teams_messages',
21
21
  mcp_tier: :standard,
22
22
  idempotent: true,
23
- inputs: { properties: { chat_id: { type: 'string',
24
- description: 'Teams chat ID' } },
23
+ inputs: { properties: { chat_id: { type: 'string',
24
+ description: 'Teams chat ID' },
25
+ top: { type: 'integer',
26
+ description: 'Messages per page (default 50, max 50)' },
27
+ max_pages: { type: 'integer',
28
+ description: 'Maximum pages to fetch (default 1)' },
29
+ orderby: { type: 'string',
30
+ description: 'Sort order: lastModifiedDateTime desc or createdDateTime desc' },
31
+ filter: { type: 'string',
32
+ description: 'OData $filter on lastModifiedDateTime or createdDateTime' } },
25
33
  required: ['chat_id'] },
26
34
  trigger_words: %w[messages history read]
27
35
 
28
- def list_chat_messages(chat_id:, top: 50, **)
29
- params = { '$top' => top }
30
- response = graph_connection(**).get("chats/#{chat_id}/messages", params)
31
- { result: response.body }
36
+ def list_chat_messages(chat_id:, top: 50, max_pages: 1, orderby: nil, filter: nil, **)
37
+ log.debug "list_chat_messages(chat_id: #{chat_id}, top: #{top}, max_pages: #{max_pages})"
38
+ per_page = [top, 50].min
39
+ params = { '$top' => per_page }
40
+ params['$orderby'] = orderby if orderby
41
+ params['$filter'] = filter if filter
42
+ conn = graph_connection(**)
43
+ response = conn.get("chats/#{chat_id}/messages", params)
44
+ body = response.body
45
+
46
+ return { result: body } if max_pages <= 1
47
+
48
+ all_values = Array(body['value'] || body[:value])
49
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
50
+ pages_fetched = 1
51
+
52
+ while next_link && pages_fetched < max_pages
53
+ response = conn.get(next_link)
54
+ page_body = response.body
55
+ items = page_body['value'] || page_body[:value]
56
+ all_values.concat(Array(items)) if items
57
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
58
+ pages_fetched += 1
59
+ end
60
+
61
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
62
+ 'value' => all_values }
63
+ result['@odata.nextLink'] = next_link if next_link
64
+ { result: result }
32
65
  end
33
66
 
34
67
  definition :get_chat_message,
@@ -43,6 +76,7 @@ module Legion
43
76
  trigger_words: %w[message fetch]
44
77
 
45
78
  def get_chat_message(chat_id:, message_id:, **)
79
+ log.debug "get_chat_message(chat_id: #{chat_id}, message_id: #{message_id})"
46
80
  response = graph_connection(**).get("chats/#{chat_id}/messages/#{message_id}")
47
81
  { result: response.body }
48
82
  end
@@ -60,6 +94,7 @@ module Legion
60
94
  trigger_words: %w[send post write]
61
95
 
62
96
  def send_chat_message(chat_id:, content:, content_type: 'text', attachments: [], **)
97
+ log.debug "send_chat_message(chat_id: #{chat_id}, content: #{content}, content_type: #{content_type})"
63
98
  payload = { body: { contentType: content_type, content: content } }
64
99
  payload[:attachments] = attachments unless attachments.empty?
65
100
  response = graph_connection(**).post("chats/#{chat_id}/messages", payload)
@@ -79,26 +114,54 @@ module Legion
79
114
  trigger_words: %w[reply respond]
80
115
 
81
116
  def reply_to_chat_message(chat_id:, message_id:, content:, content_type: 'text', **)
117
+ log.debug "reply_to_chat_message(chat_id: #{chat_id}, message_id: #{message_id}, content: #{content}, content_type: #{content_type})"
82
118
  payload = { body: { contentType: content_type, content: content } }
83
119
  response = graph_connection(**).post("chats/#{chat_id}/messages/#{message_id}/replies", payload)
84
120
  { result: response.body }
85
121
  end
86
122
 
87
123
  definition :list_message_replies,
88
- desc: 'List replies to a message in a Teams chat',
124
+ desc: 'List replies to a message in a Teams chat with pagination support',
89
125
  mcp_prefix: 'teams.list_message_replies',
90
126
  mcp_category: 'teams_messages',
91
127
  mcp_tier: :standard,
92
128
  idempotent: true,
93
129
  inputs: { properties: { chat_id: { type: 'string' },
94
- message_id: { type: 'string' } },
130
+ message_id: { type: 'string' },
131
+ top: { type: 'integer',
132
+ description: 'Number of replies to return per page (default 50)' },
133
+ max_pages: { type: 'integer',
134
+ description: 'Maximum pages to fetch (default 1)' } },
95
135
  required: %w[chat_id message_id] },
96
136
  trigger_words: %w[replies thread]
97
137
 
98
- def list_message_replies(chat_id:, message_id:, top: 50, **)
99
- params = { '$top' => top }
100
- response = graph_connection(**).get("chats/#{chat_id}/messages/#{message_id}/replies", params)
101
- { result: response.body }
138
+ def list_message_replies(chat_id:, message_id:, top: 50, max_pages: 1, **)
139
+ log.debug "list_message_replies(chat_id: #{chat_id}, message_id: #{message_id}, top: #{top}, max_pages: #{max_pages})"
140
+ per_page = [top, 50].min
141
+ params = { '$top' => per_page }
142
+ conn = graph_connection(**)
143
+ response = conn.get("chats/#{chat_id}/messages/#{message_id}/replies", params)
144
+ body = response.body
145
+
146
+ return { result: body } if max_pages <= 1
147
+
148
+ all_values = Array(body['value'] || body[:value])
149
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
150
+ pages_fetched = 1
151
+
152
+ while next_link && pages_fetched < max_pages
153
+ response = conn.get(next_link)
154
+ page_body = response.body
155
+ items = page_body['value'] || page_body[:value]
156
+ all_values.concat(Array(items)) if items
157
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
158
+ pages_fetched += 1
159
+ end
160
+
161
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
162
+ 'value' => all_values }
163
+ result['@odata.nextLink'] = next_link if next_link
164
+ { result: result }
102
165
  end
103
166
 
104
167
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Ownership
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  TEAMS_FILTER = "resourceProvisioningOptions/Any(x:x eq 'Team')"
@@ -32,16 +32,28 @@ module Legion
32
32
  end
33
33
 
34
34
  definition :list_people,
35
- desc: 'List people relevant to the current user (colleagues, contacts)',
35
+ desc: 'List people relevant to the current user with search and filter support',
36
36
  mcp_prefix: 'teams.list_people',
37
37
  mcp_category: 'teams_people',
38
38
  mcp_tier: :standard,
39
39
  idempotent: true,
40
+ inputs: { properties: { top: { type: 'integer',
41
+ description: 'Number of people to return (default 25)' },
42
+ search: { type: 'string',
43
+ description: 'Search term to find people by name or email' },
44
+ filter: { type: 'string',
45
+ description: 'OData $filter expression' },
46
+ select: { type: 'string',
47
+ description: 'Comma-separated fields to return' } },
48
+ required: [] },
40
49
  trigger_words: %w[people colleagues contacts]
41
50
 
42
- def list_people(user_id: 'me', top: 25, **)
51
+ def list_people(user_id: 'me', top: 25, search: nil, filter: nil, select: nil, **)
43
52
  log.debug("People#list_people user_id=#{user_id} top=#{top}")
44
53
  params = { '$top' => top }
54
+ params['$search'] = "\"#{search}\"" if search
55
+ params['$filter'] = filter if filter
56
+ params['$select'] = select if select
45
57
  response = graph_connection(**).get("#{user_path(user_id)}/people", params)
46
58
  { result: response.body }
47
59
  rescue StandardError => e
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Presence
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -241,7 +241,13 @@ module Legion
241
241
 
242
242
  if defined?(Legion::Extensions::Transformer::Client)
243
243
  client = Legion::Extensions::Transformer::Client.new
244
- result = client.transform(text: text, **definition)
244
+ result = client.transform(
245
+ payload: text,
246
+ transformation: definition[:prompt],
247
+ schema: definition[:schema],
248
+ engine_options: definition[:engine_options] || {},
249
+ name: definition[:name]
250
+ )
245
251
  result[:result] || result[:error] ? nil : result
246
252
  elsif defined?(Legion::LLM)
247
253
  llm_ask(message: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Subscriptions
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Teams
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  def self.trigger_words
@@ -14,15 +15,23 @@ module Legion
14
15
  end
15
16
 
16
17
  definition :list_joined_teams,
17
- desc: 'List Teams the current user has joined',
18
+ desc: 'List Teams the current user has joined with optional filtering and select',
18
19
  mcp_prefix: 'teams.list_joined_teams',
19
20
  mcp_category: 'teams_teams',
20
21
  mcp_tier: :low,
21
22
  idempotent: true,
23
+ inputs: { properties: { filter: { type: 'string',
24
+ description: 'OData $filter expression' },
25
+ select: { type: 'string',
26
+ description: 'Comma-separated fields to return' } },
27
+ required: [] },
22
28
  trigger_words: %w[teams joined membership]
23
29
 
24
- def list_joined_teams(user_id: 'me', **)
25
- response = graph_connection(**).get("#{user_path(user_id)}/joinedTeams")
30
+ def list_joined_teams(user_id: 'me', filter: nil, select: nil, **)
31
+ params = {}
32
+ params['$filter'] = filter if filter
33
+ params['$select'] = select if select
34
+ response = graph_connection(**).get("#{user_path(user_id)}/joinedTeams", params)
26
35
  { result: response.body }
27
36
  end
28
37
 
@@ -41,17 +50,47 @@ module Legion
41
50
  end
42
51
 
43
52
  definition :list_team_members,
44
- desc: 'List members of a Team',
53
+ desc: 'List members of a Team with pagination',
45
54
  mcp_prefix: 'teams.list_team_members',
46
55
  mcp_category: 'teams_teams',
47
56
  mcp_tier: :standard,
48
57
  idempotent: true,
49
- inputs: { properties: { team_id: { type: 'string' } }, required: ['team_id'] },
58
+ inputs: { properties: { team_id: { type: 'string' },
59
+ top: { type: 'integer',
60
+ description: 'Members per page (default 100)' },
61
+ max_pages: { type: 'integer',
62
+ description: 'Maximum pages to fetch (default 1)' },
63
+ filter: { type: 'string',
64
+ description: 'OData $filter expression' } },
65
+ required: ['team_id'] },
50
66
  trigger_words: %w[members roster]
51
67
 
52
- def list_team_members(team_id:, **)
53
- response = graph_connection(**).get("teams/#{team_id}/members")
54
- { result: response.body }
68
+ def list_team_members(team_id:, top: 100, max_pages: 1, filter: nil, **)
69
+ params = { '$top' => top }
70
+ params['$filter'] = filter if filter
71
+ conn = graph_connection(**)
72
+ response = conn.get("teams/#{team_id}/members", params)
73
+ body = response.body
74
+
75
+ return { result: body } if max_pages <= 1
76
+
77
+ all_values = Array(body['value'] || body[:value])
78
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
79
+ pages_fetched = 1
80
+
81
+ while next_link && pages_fetched < max_pages
82
+ response = conn.get(next_link)
83
+ page_body = response.body
84
+ items = page_body['value'] || page_body[:value]
85
+ all_values.concat(Array(items)) if items
86
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
87
+ pages_fetched += 1
88
+ end
89
+
90
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
91
+ 'value' => all_values }
92
+ result['@odata.nextLink'] = next_link if next_link
93
+ { result: result }
55
94
  end
56
95
 
57
96
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -7,6 +7,7 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Transcripts
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
12
13
  CONTENT_TYPES = {
@@ -19,17 +20,44 @@ module Legion
19
20
  end
20
21
 
21
22
  definition :list_transcripts,
22
- desc: 'List transcripts for an online meeting',
23
+ desc: 'List transcripts for an online meeting with pagination',
23
24
  mcp_prefix: 'teams.list_transcripts',
24
25
  mcp_category: 'teams_meetings',
25
26
  mcp_tier: :standard,
26
27
  idempotent: true,
27
- inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
28
+ inputs: { properties: { meeting_id: { type: 'string' },
29
+ top: { type: 'integer',
30
+ description: 'Transcripts per page (default 50)' },
31
+ max_pages: { type: 'integer',
32
+ description: 'Maximum pages to fetch (default 1)' } },
33
+ required: ['meeting_id'] },
28
34
  trigger_words: ['transcripts']
29
35
 
30
- def list_transcripts(meeting_id:, user_id: 'me', **)
31
- response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts")
32
- { result: response.body }
36
+ def list_transcripts(meeting_id:, user_id: 'me', top: 50, max_pages: 1, **)
37
+ params = { '$top' => top }
38
+ conn = graph_connection(**)
39
+ response = conn.get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts", params)
40
+ body = response.body
41
+
42
+ return { result: body } if max_pages <= 1
43
+
44
+ all_values = Array(body['value'] || body[:value])
45
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
46
+ pages_fetched = 1
47
+
48
+ while next_link && pages_fetched < max_pages
49
+ response = conn.get(next_link)
50
+ page_body = response.body
51
+ items = page_body['value'] || page_body[:value]
52
+ all_values.concat(Array(items)) if items
53
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
54
+ pages_fetched += 1
55
+ end
56
+
57
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
58
+ 'value' => all_values }
59
+ result['@odata.nextLink'] = next_link if next_link
60
+ { result: result }
33
61
  end
34
62
 
35
63
  definition :get_transcript,
@@ -57,7 +85,9 @@ module Legion
57
85
  mcp_tier: :standard,
58
86
  idempotent: true,
59
87
  inputs: { properties: { meeting_id: { type: 'string' },
60
- transcript_id: { type: 'string' } },
88
+ transcript_id: { type: 'string' },
89
+ format: { type: 'string',
90
+ description: 'Output format: vtt (default) or docx' } },
61
91
  required: %w[meeting_id transcript_id] },
62
92
  trigger_words: %w[content vtt text read]
63
93
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.6.47'
6
+ VERSION = '0.6.50'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-microsoft_teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.47
4
+ version: 0.6.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity